diff --git a/libprs500/gui/__init__.py b/libprs500/gui/__init__.py index 65fb6807c2..80a795bef6 100644 --- a/libprs500/gui/__init__.py +++ b/libprs500/gui/__init__.py @@ -12,6 +12,7 @@ ## 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. +""" The GUI to libprs500. Also has ebook library management features. """ __docformat__ = "epytext" __author__ = "Kovid Goyal " APP_TITLE = "libprs500" @@ -23,34 +24,36 @@ from PyQt4 import QtCore, QtGui # Needed in globals() for import_ui error_dialog = None def extension(path): - return os.path.splitext(path)[1][1:].lower() + return os.path.splitext(path)[1][1:].lower() def installErrorHandler(dialog): - global error_dialog - error_dialog = dialog - error_dialog.resize(600, 400) - error_dialog.setWindowTitle(APP_TITLE + " - Error") - error_dialog.setModal(True) - + global error_dialog + error_dialog = dialog + error_dialog.resize(600, 400) + error_dialog.setWindowTitle(APP_TITLE + " - Error") + error_dialog.setModal(True) -def Warning(msg, e): - print >> sys.stderr, msg - if e: traceback.print_exc(e) + +def _Warning(msg, e): + print >> sys.stderr, msg + if e: traceback.print_exc(e) def Error(msg, e): - if error_dialog: - if e: msg += "
" + traceback.format_exc(e) - msg = re.sub("Traceback", "Traceback", msg) - msg = re.sub(r"\n", "
", msg) - error_dialog.showMessage(msg) - error_dialog.show() + if error_dialog: + if e: msg += "
" + traceback.format_exc(e) + msg = re.sub("Traceback", "Traceback", msg) + msg = re.sub(r"\n", "
", msg) + error_dialog.showMessage(msg) + error_dialog.show() def import_ui(name): - uifile = pkg_resources.resource_stream(__name__, name) - code_string = StringIO.StringIO() - winfo = compiler.UICompiler().compileUi(uifile, code_string) - ui = pkg_resources.resource_filename(__name__, name) - exec code_string.getvalue() - return locals()[winfo["uiclass"]] + uifile = pkg_resources.resource_stream(__name__, name) + code_string = StringIO.StringIO() + winfo = compiler.UICompiler().compileUi(uifile, code_string) + ui = pkg_resources.resource_filename(__name__, name) + exec code_string.getvalue() + return locals()[winfo["uiclass"]] -from libprs500.gui.widgets import LibraryBooksView, DeviceBooksView, CoverDisplay, DeviceView # Needed in globals() for import_ui +# Needed in globals() for import_ui +from libprs500.gui.widgets import LibraryBooksView, \ + DeviceBooksView, CoverDisplay, DeviceView diff --git a/libprs500/gui/database.py b/libprs500/gui/database.py index 68704b3616..4477e7a077 100644 --- a/libprs500/gui/database.py +++ b/libprs500/gui/database.py @@ -20,146 +20,189 @@ from libprs500.lrf.meta import LRFMetaFile from cStringIO import StringIO as cStringIO class LibraryDatabase(object): - - BOOKS_SQL = """ - create table if not exists books_meta(id INTEGER PRIMARY KEY, title TEXT, authors TEXT, publisher TEXT, size INTEGER, tags TEXT, - cover BLOB, date DATE DEFAULT CURRENT_TIMESTAMP, comments TEXT, rating INTEGER); - create table if not exists books_data(id INTEGER, extension TEXT, data BLOB); - """ - - def __init__(self, dbpath): - self.con = sqlite.connect(dbpath) - self.con.row_factory = sqlite.Row # Allow case insensitive field access by name - self.con.executescript(LibraryDatabase.BOOKS_SQL) - - def get_cover(self, id): - raw = self.con.execute("select cover from books_meta where id=?", (id,)).next()["cover"] - return decompress(str(raw)) if raw else None - def get_extensions(self, id): - exts = [] - cur = self.con.execute("select extension from books_data where id=?", (id,)) - for row in cur: - exts.append(row["extension"]) - return exts - - def add_book(self, path): - file = os.path.abspath(path) - title, author, publisher, size, cover = os.path.basename(file), None, None, os.stat(file)[ST_SIZE], None - ext = title[title.rfind(".")+1:].lower() if title.find(".") > -1 else None - if ext == "lrf": - lrf = LRFMetaFile(open(file, "r+b")) - title, author, cover, publisher = lrf.title, lrf.author.strip(), lrf.thumbnail, lrf.publisher.strip() - if "unknown" in publisher.lower(): publisher = None - if "unknown" in author.lower(): author = None - file = compress(open(file).read()) - if cover: cover = sqlite.Binary(compress(cover)) - self.con.execute("insert into books_meta (title, authors, publisher, size, tags, cover, comments, rating) values (?,?,?,?,?,?,?,?)", (title, author, publisher, size, None, cover, None, None)) - id = self.con.execute("select max(id) from books_meta").next()[0] - self.con.execute("insert into books_data values (?,?,?)", (id, ext, sqlite.Binary(file))) - self.con.commit() - return id - - def get_row_by_id(self, id, columns): - """ @param columns: list of column names """ - cols = ",".join([ c for c in columns]) - cur = self.con.execute("select " + cols + " from books_meta where id=?", (id,)) - row, r = cur.next(), {} - for c in columns: r[c] = row[c] - return r - - def commit(self): self.con.commit() - - def delete_by_id(self, id): - self.con.execute("delete from books_meta where id=?", (id,)) - self.con.execute("delete from books_data where id=?", (id,)) - - def get_table(self, columns): - cols = ",".join([ c for c in columns]) - cur = self.con.execute("select " + cols + " from books_meta") - rows = [] - for row in cur: - r = {} - for c in columns: r[c] = row[c] - rows.append(r) - return rows - - def get_format(self, id, ext): - """ - Return format C{ext} corresponding to the logical book C{id} or None if the format is unavailable. - Format is returned as a string of binary data suitable for C{ file.write} operations. - """ - ext = ext.lower() - cur = self.con.execute("select data from books_data where id=? and extension=?",(id, ext)) - try: data = cur.next() - except: pass - else: return decompress(str(data["data"])) - - def add_format(self, id, ext, data): + BOOKS_SQL = """ + create table if not exists books_meta(id INTEGER PRIMARY KEY, title TEXT, + authors TEXT, publisher TEXT, size INTEGER, tags TEXT, + cover BLOB, date DATE DEFAULT CURRENT_TIMESTAMP, + comments TEXT, rating INTEGER); + create table if not exists books_data(id INTEGER, extension TEXT, data BLOB); """ - If data for format ext already exists, it is replaced - @type ext: string or None - @type data: string - """ - try: - data.seek(0) - data = data.read() - except AttributeError: pass - if ext: ext = ext.strip().lower() - data = sqlite.Binary(compress(data)) - cur = self.con.execute("select extension from books_data where id=? and extension=?", (id, ext)) - present = True - try: cur.next() - except: present = False - if present: - self.con.execute("update books_data set data=? where id=? and extension=?", (data, id, ext)) - else: - self.con.execute("insert into books_data (id, extension, data) values (?, ?, ?)", (id, ext, data)) - self.con.commit() - - def get_meta_data(self, id): - try: row = self.con.execute("select * from books_meta where id=?", (id,)).next() - except StopIteration: return None - data = {} - for field in ("id", "title", "authors", "publisher", "size", "tags", "cover", "date"): - data[field] = row[field] - return data - def set_metadata(self, id, title=None, authors=None, rating=None, publisher=None, tags=None, cover=None, comments=None): - if authors and not len(authors): authors = None - if publisher and not len(publisher): publisher = None - if tags and not len(tags): tags = None - if comments and not len(comments): comments = None - if cover: cover = sqlite.Binary(compress(cover)) - self.con.execute('update books_meta set title=?, authors=?, publisher=?, tags=?, cover=?, comments=?, rating=? where id=?', (title, authors, publisher, tags, cover, comments, rating, id)) - self.con.commit() - - def set_metadata_item(self, id, col, val): - self.con.execute('update books_meta set '+col+'=? where id=?',(val, id)) - if col in ["authors", "title"]: - lrf = self.get_format(id, "lrf") - if lrf: - c = cStringIO() - c.write(lrf) - lrf = LRFMetaFile(c) - if col == "authors": lrf.authors = val - else: lrf.title = val - self.add_format(id, "lrf", c.getvalue()) - self.con.commit() + def __init__(self, dbpath): + self.con = sqlite.connect(dbpath) + # Allow case insensitive field access by name + self.con.row_factory = sqlite.Row + self.con.executescript(LibraryDatabase.BOOKS_SQL) - def update_cover(self, id, cover): - data = None - if cover: data = sqlite.Binary(compress(cover)) - self.con.execute('update books_meta set cover=? where id=?', (data, id)) - lrf = self.get_format(id, "lrf") - if lrf: - c = cStringIO() - c.write(lrf) - lrf = LRFMetaFile(c) - lrf.thumbnail = cover - self.add_format(id, "lrf", c.getvalue()) - self.commit() - + def get_cover(self, _id): + raw = self.con.execute("select cover from books_meta where id=?", (_id,))\ + .next()["cover"] + return decompress(str(raw)) if raw else None + + def get_extensions(self, _id): + exts = [] + cur = self.con.execute("select extension from books_data where id=?", (_id,)) + for row in cur: + exts.append(row["extension"]) + return exts + + def add_book(self, path): + _file = os.path.abspath(path) + title, author, publisher, size, cover = os.path.basename(_file), \ + None, None, os.stat(_file)[ST_SIZE], None + ext = title[title.rfind(".")+1:].lower() if title.find(".") > -1 else None + if ext == "lrf": + lrf = LRFMetaFile(open(_file, "r+b")) + title, author, cover, publisher = lrf.title, lrf.author.strip(), \ + lrf.thumbnail, lrf.publisher.strip() + if "unknown" in publisher.lower(): + publisher = None + if "unknown" in author.lower(): + author = None + _file = compress(open(_file).read()) + if cover: + cover = sqlite.Binary(compress(cover)) + self.con.execute("insert into books_meta (title, authors, publisher, "+\ + "size, tags, cover, comments, rating) values "+\ + "(?,?,?,?,?,?,?,?)", \ + (title, author, publisher, size, None, cover, None, None)) + _id = self.con.execute("select max(id) from books_meta").next()[0] + self.con.execute("insert into books_data values (?,?,?)", \ + (_id, ext, sqlite.Binary(_file))) + self.con.commit() + return _id + + def get_row_by_id(self, _id, columns): + """ @param columns: list of column names """ + cols = ",".join([ c for c in columns]) + cur = self.con.execute("select " + cols + " from books_meta where id=?"\ + , (_id,)) + row, r = cur.next(), {} + for c in columns: + r[c] = row[c] + return r + + def commit(self): self.con.commit() + + def delete_by_id(self, _id): + self.con.execute("delete from books_meta where id=?", (_id,)) + self.con.execute("delete from books_data where id=?", (_id,)) + + def get_table(self, columns): + cols = ",".join([ c for c in columns]) + cur = self.con.execute("select " + cols + " from books_meta") + rows = [] + for row in cur: + r = {} + for c in columns: + r[c] = row[c] + rows.append(r) + return rows + + def get_format(self, _id, ext): + """ + Return format C{ext} corresponding to the logical book C{id} or + None if the format is unavailable. + Format is returned as a string of binary data suitable for + C{ file.write} operations. + """ + ext = ext.lower() + cur = self.con.execute("select data from books_data where id=? and "+\ + "extension=?",(_id, ext)) + try: + data = cur.next() + except: + pass + else: + return decompress(str(data["data"])) + + def add_format(self, _id, ext, data): + """ + If data for format ext already exists, it is replaced + @type ext: string or None + @type data: string + """ + try: + data.seek(0) + data = data.read() + except AttributeError: pass + if ext: ext = ext.strip().lower() + data = sqlite.Binary(compress(data)) + cur = self.con.execute("select extension from books_data where id=? "+\ + "and extension=?", (_id, ext)) + present = True + try: cur.next() + except: present = False + if present: + self.con.execute("update books_data set data=? where id=? "+\ + "and extension=?", (data, _id, ext)) + else: + self.con.execute("insert into books_data (id, extension, data) "+\ + "values (?, ?, ?)", (_id, ext, data)) + self.con.commit() + + def get_meta_data(self, _id): + try: + row = self.con.execute("select * from books_meta where id=?", \ + (_id,)).next() + except StopIteration: + return None + data = {} + for field in ("id", "title", "authors", "publisher", "size", "tags", + "cover", "date"): + data[field] = row[field] + return data + + def set_metadata(self, _id, title=None, authors=None, rating=None, \ + publisher=None, tags=None, cover=None, \ + comments=None): + if authors and not len(authors): + authors = None + if publisher and not len(publisher): + publisher = None + if tags and not len(tags): + tags = None + if comments and not len(comments): + comments = None + if cover: + cover = sqlite.Binary(compress(cover)) + self.con.execute('update books_meta set title=?, authors=?, '+\ + 'publisher=?, tags=?, cover=?, comments=?, rating=? '+\ + 'where id=?', \ + (title, authors, publisher, tags, cover, comments, \ + rating, _id)) + self.con.commit() + + def set_metadata_item(self, _id, col, val): + self.con.execute('update books_meta set '+col+'=? where id=?', \ + (val, _id)) + if col in ["authors", "title"]: + lrf = self.get_format(_id, "lrf") + if lrf: + c = cStringIO() + c.write(lrf) + lrf = LRFMetaFile(c) + if col == "authors": + lrf.authors = val + else: lrf.title = val + self.add_format(_id, "lrf", c.getvalue()) + self.con.commit() + + def 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__": diff --git a/libprs500/gui/editbook.py b/libprs500/gui/editbook.py index 6faa828485..0aa6c418b5 100644 --- a/libprs500/gui/editbook.py +++ b/libprs500/gui/editbook.py @@ -20,114 +20,114 @@ from libprs500.lrf.meta import LRFMetaFile from libprs500.gui import import_ui class Format(QListWidgetItem): - def __init__(self, parent, ext, data): - self.data = data - self.ext = ext - QListWidgetItem.__init__(self, ext.upper(), parent, QListWidgetItem.UserType) + def __init__(self, parent, ext, data): + self.data = data + self.ext = ext + QListWidgetItem.__init__(self, ext.upper(), parent, QListWidgetItem.UserType) Ui_BookEditDialog = import_ui("editbook.ui") class EditBookDialog(Ui_BookEditDialog): - - def select_cover(self, checked): - settings = QSettings() - dir = settings.value("change cover dir", QVariant(os.path.expanduser("~"))).toString() - file = QFileDialog.getOpenFileName(self.window, "Choose cover for " + str(self.title.text()), dir, "Images (*.png *.gif *.jpeg *.jpg);;All files (*)") - if len(str(file)): - file = os.path.abspath(file) - settings.setValue("change cover dir", QVariant(os.path.dirname(file))) - if not os.access(file, os.R_OK): - QErrorMessage(self.parent).showMessage("You do not have permission to read the file: " + file) - return - cf, cover = None, None - try: - cf = open(file, "rb") - cover = cf.read() - except IOError, e: QErrorMessage(self.parent).showMessage("There was an error reading from file: " + file + "\n"+str(e)) - if cover: - pix = QPixmap() - pix.loadFromData(cover, "", Qt.AutoColor) - if pix.isNull(): QErrorMessage(self.parent).showMessage(file + " is not a valid picture") - else: - self.cover_path.setText(file) - self.cover.setPixmap(pix) - self.cover_data = cover - - - def write_data(self): - title = str(self.title.text()).strip() - authors = str(self.authors.text()).strip() - rating = self.rating.value() - tags = str(self.tags.text()).strip() - publisher = str(self.publisher.text()).strip() - comments = str(self.comments.toPlainText()).strip() - self.db.set_metadata(self.id, title=title, authors=authors, rating=rating, tags=tags, publisher=publisher, comments=comments, cover=self.cover_data) - if self.formats_changed: - for r in range(self.formats.count()): - format = self.formats.item(r) - self.db.add_format(self.id, format.ext, format.data) - lrf = self.db.get_format(self.id, "lrf") - if lrf: - lrf = StringIO.StringIO(lrf) - lf = LRFMetaFile(lrf) - if title: lf.title = title - if authors: lf.title = authors - if publisher: lf.publisher = publisher - if self.cover_data: lf.thumbnail = self.cover_data - self.db.add_format(self.id, "lrf", lrf.getvalue()) - - def add_format(self, x): - dir = settings.value("add formats dialog dir", QVariant(os.path.expanduser("~"))).toString() - files = QFileDialog.getOpenFileNames(self.window, "Choose formats for " + str(self.title.text()), dir, "Books (*.lrf *.lrx *.rtf *.txt *.html *.xhtml *.htm *.rar);;All files (*)") - if not files.isEmpty(): - x = str(files[0]) - settings.setValue("add formats dialog dir", QVariant(os.path.dirname(x))) - files = str(files.join("|||")).split("|||") - for file in files: - file = os.path.abspath(file) - if not os.access(file, os.R_OK): - QErrorMessage(self.parent).showMessage("You do not have permission to read the file: " + file) - continue - f, data = None, None - try: - f = open(file, "rb") - data = f.read() - except IOError, e: QErrorMessage(self.parent).showMessage("There was an error reading from file: " + file + "\n"+str(e)) - if data: - ext = file[file.rfind(".")+1:].lower() if file.find(".") > -1 else None - Format(self.formats, ext, data) - self.formats_changed = True + def select_cover(self, checked): + settings = QSettings() + dir = settings.value("change cover dir", QVariant(os.path.expanduser("~"))).toString() + file = QFileDialog.getOpenFileName(self.window, "Choose cover for " + str(self.title.text()), dir, "Images (*.png *.gif *.jpeg *.jpg);;All files (*)") + if len(str(file)): + file = os.path.abspath(file) + settings.setValue("change cover dir", QVariant(os.path.dirname(file))) + if not os.access(file, os.R_OK): + QErrorMessage(self.parent).showMessage("You do not have permission to read the file: " + file) + return + cf, cover = None, None + try: + cf = open(file, "rb") + cover = cf.read() + except IOError, e: QErrorMessage(self.parent).showMessage("There was an error reading from file: " + file + "\n"+str(e)) + if cover: + pix = QPixmap() + pix.loadFromData(cover, "", Qt.AutoColor) + if pix.isNull(): QErrorMessage(self.parent).showMessage(file + " is not a valid picture") + else: + self.cover_path.setText(file) + self.cover.setPixmap(pix) + self.cover_data = cover - def remove_format(self, x): - rows = self.formats.selectionModel().selectedRows(0) - for row in rows: - item = self.formats.takeItem(row.row()) - self.formats_changed = True - - def __init__(self, dialog, id, db): - Ui_BookEditDialog.__init__(self) - self.parent = dialog - self.setupUi(dialog) - self.splitter.setStretchFactor(100,1) - self.db = db - self.id = id - self.cover_data = None - self.formats_changed = False - QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), self.select_cover) - QObject.connect(self.button_box, SIGNAL("accepted()"), self.write_data) - QObject.connect(self.add_format_button, SIGNAL("clicked(bool)"), self.add_format) - QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), self.remove_format) - data = self.db.get_row_by_id(self.id, ["title","authors","rating","publisher","tags","comments"]) - self.title.setText(data["title"]) - self.authors.setText(data["authors"] if data["authors"] else "") - self.publisher.setText(data["publisher"] if data["publisher"] else "") - self.tags.setText(data["tags"] if data["tags"] else "") - if data["rating"] > 0: self.rating.setValue(data["rating"]) - self.comments.setPlainText(data["comments"] if data["comments"] else "") - cover = self.db.get_cover(self.id) - if cover: - pm = QPixmap() - pm.loadFromData(cover, "", Qt.AutoColor) - self.cover.setPixmap(pm) - else: - self.cover.setPixmap(QPixmap(":/default_cover")) + + def write_data(self): + title = str(self.title.text()).strip() + authors = str(self.authors.text()).strip() + rating = self.rating.value() + tags = str(self.tags.text()).strip() + publisher = str(self.publisher.text()).strip() + comments = str(self.comments.toPlainText()).strip() + self.db.set_metadata(self.id, title=title, authors=authors, rating=rating, tags=tags, publisher=publisher, comments=comments, cover=self.cover_data) + if self.formats_changed: + for r in range(self.formats.count()): + format = self.formats.item(r) + self.db.add_format(self.id, format.ext, format.data) + lrf = self.db.get_format(self.id, "lrf") + if lrf: + lrf = StringIO.StringIO(lrf) + lf = LRFMetaFile(lrf) + if title: lf.title = title + if authors: lf.title = authors + if publisher: lf.publisher = publisher + if self.cover_data: lf.thumbnail = self.cover_data + self.db.add_format(self.id, "lrf", lrf.getvalue()) + + + def add_format(self, x): + dir = settings.value("add formats dialog dir", QVariant(os.path.expanduser("~"))).toString() + files = QFileDialog.getOpenFileNames(self.window, "Choose formats for " + str(self.title.text()), dir, "Books (*.lrf *.lrx *.rtf *.txt *.html *.xhtml *.htm *.rar);;All files (*)") + if not files.isEmpty(): + x = str(files[0]) + settings.setValue("add formats dialog dir", QVariant(os.path.dirname(x))) + files = str(files.join("|||")).split("|||") + for file in files: + file = os.path.abspath(file) + if not os.access(file, os.R_OK): + QErrorMessage(self.parent).showMessage("You do not have permission to read the file: " + file) + continue + f, data = None, None + try: + f = open(file, "rb") + data = f.read() + except IOError, e: QErrorMessage(self.parent).showMessage("There was an error reading from file: " + file + "\n"+str(e)) + if data: + ext = file[file.rfind(".")+1:].lower() if file.find(".") > -1 else None + Format(self.formats, ext, data) + self.formats_changed = True + + def remove_format(self, x): + rows = self.formats.selectionModel().selectedRows(0) + for row in rows: + item = self.formats.takeItem(row.row()) + self.formats_changed = True + + def __init__(self, dialog, id, db): + Ui_BookEditDialog.__init__(self) + self.parent = dialog + self.setupUi(dialog) + self.splitter.setStretchFactor(100,1) + self.db = db + self.id = id + self.cover_data = None + self.formats_changed = False + QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), self.select_cover) + QObject.connect(self.button_box, SIGNAL("accepted()"), self.write_data) + QObject.connect(self.add_format_button, SIGNAL("clicked(bool)"), self.add_format) + QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), self.remove_format) + data = self.db.get_row_by_id(self.id, ["title","authors","rating","publisher","tags","comments"]) + self.title.setText(data["title"]) + self.authors.setText(data["authors"] if data["authors"] else "") + self.publisher.setText(data["publisher"] if data["publisher"] else "") + self.tags.setText(data["tags"] if data["tags"] else "") + if data["rating"] > 0: self.rating.setValue(data["rating"]) + self.comments.setPlainText(data["comments"] if data["comments"] else "") + cover = self.db.get_cover(self.id) + if cover: + pm = QPixmap() + pm.loadFromData(cover, "", Qt.AutoColor) + self.cover.setPixmap(pm) + else: + self.cover.setPixmap(QPixmap(":/default_cover")) diff --git a/libprs500/gui/main.py b/libprs500/gui/main.py index f05e991d4d..f62f9fefe2 100644 --- a/libprs500/gui/main.py +++ b/libprs500/gui/main.py @@ -11,466 +11,541 @@ ## ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., -## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QSettings, QVariant, QSize, QEventLoop, QString, QBuffer, QIODevice, QModelIndex -from PyQt4.QtGui import QPixmap, QAbstractItemView, QErrorMessage, QMessageBox, QFileDialog, QIcon -from PyQt4.Qt import qInstallMsgHandler, qDebug, qFatal, qWarning, qCritical -from PyQt4 import uic +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.Warning +""" Create and launch the GUI """ +import sys +import re +import os +import traceback +import tempfile + +from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ + QSettings, QVariant, QSize, QEventLoop, QString, \ + QBuffer, QIODevice, QModelIndex +from PyQt4.QtGui import QPixmap, QErrorMessage, \ + QMessageBox, QFileDialog, QIcon, QDialog +from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical from libprs500.communicate import PRS500Device as device from libprs500.books import fix_ids from libprs500.errors import * -from libprs500.lrf.meta import LRFMetaFile, LRFException -from libprs500.gui import import_ui, installErrorHandler, Error, Warning, extension, APP_TITLE -from libprs500.gui.widgets import LibraryBooksModel, DeviceBooksModel, DeviceModel, TableView +from libprs500.gui import import_ui, installErrorHandler, Error, _Warning, \ + extension, APP_TITLE +from libprs500.gui.widgets import LibraryBooksModel, DeviceBooksModel, \ + DeviceModel from database import LibraryDatabase from editbook import EditBookDialog -import sys, re, os, traceback, tempfile DEFAULT_BOOK_COVER = None -LIBRARY_BOOK_TEMPLATE = QString("
Formats: %1 Tags: %2
Comments:%3
") -DEVICE_BOOK_TEMPLATE = QString("
Title: %1 Size: %2
Author: %3 Type: %4
") +LIBRARY_BOOK_TEMPLATE = QString(" \ +
Formats: %1 \ + Tags: %2
Comments:%3
") +DEVICE_BOOK_TEMPLATE = QString("\ + \ +
Title: %1 \ +  Size: %2
Author: %3 Type: %4
") Ui_MainWindow = import_ui("main.ui") -class MainWindow(QObject, Ui_MainWindow): - - def show_device(self, yes): - """ If C{yes} show the items on the device otherwise show the items in the library """ - self.device_view.selectionModel().reset(), self.library_view.selectionModel().reset() - self.book_cover.hide(), self.book_info.hide() - if yes: - self.device_view.show(), self.library_view.hide() - self.book_cover.setAcceptDrops(False) - self.current_view = self.device_view - else: - self.device_view.hide(), self.library_view.show() - self.book_cover.setAcceptDrops(True) - self.current_view = self.library_view - self.current_view.sortByColumn(3, Qt.DescendingOrder) - - - def tree_clicked(self, index): - if index.isValid(): - self.search.clear() - show_dev = True - model = self.device_tree.model() - if model.is_library(index): - show_dev = False - elif model.is_reader(index): - self.device_view.setModel(self.reader_model) - QObject.connect(self.device_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) - elif model.is_card(index): - self.device_view.setModel(self.card_model) - QObject.connect(self.device_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) - self.show_device(show_dev) - - - def model_modified(self): - if self.library_view.isVisible(): view = self.library_view - else: view = self.device_view - view.clearSelection() - view.resizeColumnsToContents() - self.book_cover.hide() - self.book_info.hide() - QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) - - def resize_columns(self, topleft, bottomright): - if self.library_view.isVisible(): view = self.library_view - else: view = self.device_view - for c in range(topleft.column(), bottomright.column()+1): - view.resizeColumnToContents(c) - - def show_book(self, current, previous): - if self.library_view.isVisible(): - formats, tags, comments, cover = current.model().info(current.row()) - data = LIBRARY_BOOK_TEMPLATE.arg(formats).arg(tags).arg(comments) - tooltip = "To save the cover, drag it to the desktop.
To change the cover drag the new cover onto this picture" - else: - title, author, size, mime, cover = current.model().info(current.row()) - data = DEVICE_BOOK_TEMPLATE.arg(title).arg(size).arg(author).arg(mime) - tooltip = "To save the cover, drag it to the desktop." - self.book_info.setText(data) - self.book_cover.setToolTip(tooltip) - if not cover: cover = DEFAULT_BOOK_COVER - self.book_cover.setPixmap(cover) - self.book_cover.show() - self.book_info.show() - - def formats_added(self, index): - if index == self.library_view.currentIndex(): - self.show_book(index, index) - - def delete(self, action): - rows = self.current_view.selectionModel().selectedRows() - if not len(rows): return - count = str(len(rows)) - ret = QMessageBox.question(self.window, self.trUtf8(APP_TITLE + " - confirm"), self.trUtf8("Are you sure you want to permanently delete these ") +count+self.trUtf8(" item(s)?"), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) - if ret != QMessageBox.Yes: return - self.window.setCursor(Qt.WaitCursor) - if self.library_view.isVisible(): - self.library_model.delete(self.library_view.selectionModel().selectedRows()) - else: - self.status("Deleting files from device") - paths = self.device_view.model().delete(rows) - for path in paths: - self.status("Deleting "+path[path.rfind("/")+1:]) - self.dev.del_file(path, end_session=False) - fix_ids(self.reader_model.booklist, self.card_model.booklist) - self.status("Syncing media list to reader") - self.dev.upload_book_list(self.reader_model.booklist) - if len(self.card_model.booklist): - self.status("Syncing media list to card") - self.dev.upload_book_list(self.card_model.booklist) - self.update_availabe_space() - self.show_book(self.current_view.currentIndex(), QModelIndex()) - self.window.setCursor(Qt.ArrowCursor) +class Main(QObject, Ui_MainWindow): + """ Create GUI """ + def show_device(self, yes): + """ + If C{yes} show the items on the device otherwise show the items + in the library + """ + self.device_view.selectionModel().reset() + self.library_view.selectionModel().reset() + self.book_cover.hide() + self.book_info.hide() + if yes: + self.device_view.show() + self.library_view.hide() + self.book_cover.setAcceptDrops(False) + self.current_view = self.device_view + else: + self.device_view.hide() + self.library_view.show() + self.book_cover.setAcceptDrops(True) + self.current_view = self.library_view + self.current_view.sortByColumn(3, Qt.DescendingOrder) - def read_settings(self): - settings = QSettings() - settings.beginGroup("MainWindow") - self.window.resize(settings.value("size", QVariant(QSize(1000, 700))).toSize()) - settings.endGroup() - self.database_path = settings.value("database path", QVariant(os.path.expanduser("~/library.db"))).toString() - def write_settings(self): - settings = QSettings() - settings.beginGroup("MainWindow") - settings.setValue("size", QVariant(self.window.size())) - settings.endGroup() - - def close_event(self, e): - self.write_settings() - e.accept() - - def add(self, action): - settings = QSettings() - dir = settings.value("add books dialog dir", QVariant(os.path.expanduser("~"))).toString() - files = QFileDialog.getOpenFileNames(self.window, "Choose books to add to library", dir, "Books (*.lrf *.lrx *.rtf *.pdf *.txt);;All files (*)") - if not files.isEmpty(): - x = str(files[0]) - settings.setValue("add books dialog dir", QVariant(os.path.dirname(x))) - files = str(files.join("|||")).split("|||") - self.add_books(files) + def tree_clicked(self, index): + if index.isValid(): + self.search.clear() + show_dev = True + model = self.device_tree.model() + if model.is_library(index): + show_dev = False + elif model.is_reader(index): + self.device_view.setModel(self.reader_model) + QObject.connect(self.device_view.selectionModel(), \ + SIGNAL("currentChanged(QModelIndex, QModelIndex)"), \ + self.show_book) + elif model.is_card(index): + self.device_view.setModel(self.card_model) + QObject.connect(self.device_view.selectionModel(), \ + SIGNAL("currentChanged(QModelIndex, QModelIndex)"), \ + self.show_book) + self.show_device(show_dev) - def add_books(self, files): - self.window.setCursor(Qt.WaitCursor) - for file in files: - file = os.path.abspath(file) - self.library_view.model().add_book(file) - if self.library_view.isVisible(): self.search.clear() - else: self.library_model.search("") - hv = self.library_view.horizontalHeader() - col = hv.sortIndicatorSection() - order = hv.sortIndicatorOrder() - self.library_view.model().sort(col, order) - self.window.setCursor(Qt.ArrowCursor) - - def edit(self, action): - if self.library_view.isVisible(): - rows = self.library_view.selectionModel().selectedRows() - for row in rows: - id = self.library_model.id_from_index(row) - dialog = QDialog(self.window) - ed = EditBookDialog(dialog, id, self.library_model.db) - if dialog.exec_() == QDialog.Accepted: - self.library_model.refresh_row(row.row()) - - - 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: Error("Unable to change cover", e) - - def upload_books(self, to, files, ids): - oncard = False if to == "reader" else True - booklists = (self.reader_model.booklist, self.card_model.booklist) - def update_models(): - hv = self.device_view.horizontalHeader() - col = hv.sortIndicatorSection() - order = hv.sortIndicatorOrder() - model = self.card_model if oncard else self.reader_model - model.sort(col, order) - if self.device_view.isVisible() and self.device_view.model() == model: self.search.clear() - else: model.search("") - - def sync_lists(): - self.status("Syncing media list to device main memory") - self.dev.upload_book_list(booklists[0]) - if len(booklists[1]): - self.status("Syncing media list to storage card") - self.dev.upload_book_list(booklists[1]) - - self.window.setCursor(Qt.WaitCursor) - ename = "file" - try: - if ids: - for id in ids: - formats = [] - info = self.library_view.model().book_info(id) - if info["cover"]: - pix = QPixmap() - pix.loadFromData(str(info["cover"])) - if pix.isNull(): pix = DEFAULT_BOOK_COVER - pix = pix.scaledToHeight(self.dev.THUMBNAIL_HEIGHT, Qt.SmoothTransformation) - buffer = QBuffer() - buffer.open(QIODevice.WriteOnly) - pix.save(buffer, "JPEG") - info["cover"] = (pix.width(), pix.height(), str(buffer.buffer())) - ename = info["title"] - for f in files: - if re.match("......_"+str(id)+"_", os.path.basename(f)): - formats.append(f) - file = None - try: - for format in self.dev.FORMATS: - for f in formats: - if extension(f) == format: - file = f - raise StopIteration() - except StopIteration: pass - if not file: - Error("The library does not have any formats that can be viewed on the device for " + ename, None) - continue - f = open(file, "rb") - self.status("Sending "+info["title"]+" to device") - try: - self.dev.add_book(f, "libprs500_"+str(id)+"."+extension(file), info, booklists, oncard=oncard, end_session=False) - update_models() - except PathError, e: - if "already exists" in str(e): - Error(info["title"] + " already exists on the device", None) - self.progress(100) - continue - else: raise - finally: f.close() - sync_lists() - else: - for file in files: - ename = file - if extension(file) not in self.dev.FORMATS: - Error(ename + " is not in a supported format") - continue - info = { "title":os.path.basename(file), "authors":"Unknown", "cover":(None, None, None) } - f = open(file, "rb") - self.status("Sending "+info["title"]+" to device") - try: - self.dev.add_book(f, os.path.basename(file), info, booklists, oncard=oncard, end_session=False) - update_models() - except PathError, e: - if "already exists" in str(e): - Error(info["title"] + " already exists on the device", None) - self.progress(100) - continue - else: raise - finally: f.close() - sync_lists() - except Exception, e: - Error("Unable to send "+ename+" to device", e) - finally: - self.window.setCursor(Qt.ArrowCursor) - self.update_availabe_space() - - def __init__(self, window, log_packets): - QObject.__init__(self) - Ui_MainWindow.__init__(self) - - self.dev = device(report_progress=self.progress, log_packets=log_packets) - self.setupUi(window) - self.card = None - self.window = window - window.closeEvent = self.close_event - self.read_settings() + def model_modified(self): + if self.library_view.isVisible(): view = self.library_view + else: view = self.device_view + view.clearSelection() + view.resizeColumnsToContents() + self.book_cover.hide() + self.book_info.hide() + QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) - # Setup Library Book list - self.library_model = LibraryBooksModel(window) - self.library_model.set_data(LibraryDatabase(str(self.database_path))) - self.library_view.setModel(self.library_model) - self.current_view = self.library_view - QObject.connect(self.library_model, SIGNAL("layoutChanged()"), self.library_view.resizeRowsToContents) - QObject.connect(self.library_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) - QObject.connect(self.search, SIGNAL("textChanged(QString)"), self.library_model.search) - QObject.connect(self.library_model, SIGNAL("sorted()"), self.model_modified) - QObject.connect(self.library_model, SIGNAL("searched()"), self.model_modified) - QObject.connect(self.library_model, SIGNAL("deleted()"), self.model_modified) - QObject.connect(self.library_model, SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns) - QObject.connect(self.library_view, SIGNAL('books_dropped'), self.add_books) - QObject.connect(self.library_model, SIGNAL('formats_added'), self.formats_added) - self.library_view.resizeColumnsToContents() + def resize_columns(self, topleft, bottomright): + if self.library_view.isVisible(): + view = self.library_view + else: view = self.device_view + for c in range(topleft.column(), bottomright.column()+1): + view.resizeColumnToContents(c) - # Create Device tree - model = DeviceModel(self.device_tree) - QObject.connect(self.device_tree, SIGNAL("activated(QModelIndex)"), self.tree_clicked) - QObject.connect(self.device_tree, SIGNAL("clicked(QModelIndex)"), self.tree_clicked) - QObject.connect(model, SIGNAL('books_dropped'), self.add_books) - QObject.connect(model, SIGNAL('upload_books'), self.upload_books) - self.device_tree.setModel(model) + def show_book(self, current, previous): + if self.library_view.isVisible(): + formats, tags, comments, cover = current.model().info(current.row()) + data = LIBRARY_BOOK_TEMPLATE.arg(formats).arg(tags).arg(comments) + tooltip = "To save the cover, drag it to the desktop.
To \ + change the cover drag the new cover onto this picture" + else: + title, author, size, mime, cover = current.model().info(current.row()) + data = DEVICE_BOOK_TEMPLATE.arg(title).arg(size).arg(author).arg(mime) + tooltip = "To save the cover, drag it to the desktop." + self.book_info.setText(data) + self.book_cover.setToolTip(tooltip) + if not cover: cover = DEFAULT_BOOK_COVER + self.book_cover.setPixmap(cover) + self.book_cover.show() + self.book_info.show() - # Create Device Book list - self.reader_model = DeviceBooksModel(window) - self.card_model = DeviceBooksModel(window) - self.device_view.setModel(self.reader_model) - QObject.connect(self.device_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) - for model in (self.reader_model, self. card_model): - QObject.connect(model, SIGNAL("layoutChanged()"), self.device_view.resizeRowsToContents) - QObject.connect(self.search, SIGNAL("textChanged(QString)"), model.search) - QObject.connect(model, SIGNAL("sorted()"), self.model_modified) - QObject.connect(model, SIGNAL("searched()"), self.model_modified) - QObject.connect(model, SIGNAL("deleted()"), self.model_modified) - QObject.connect(model, SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns) + def formats_added(self, index): + if index == self.library_view.currentIndex(): + self.show_book(index, index) - # Setup book display - self.book_cover.hide() - self.book_info.hide() + def delete(self, action): + rows = self.current_view.selectionModel().selectedRows() + if not len(rows): + return + count = str(len(rows)) + ret = QMessageBox.question(self.window, self.trUtf8(APP_TITLE + \ + " - confirm"), self.trUtf8("Are you sure you want to \ + permanently delete these ") +count+self.trUtf8(" item(s)?"), \ + QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) + if ret != QMessageBox.Yes: + return + self.window.setCursor(Qt.WaitCursor) + if self.library_view.isVisible(): + self.library_model.delete(self.library_view.selectionModel()\ + .selectedRows()) + else: + self.status("Deleting files from device") + paths = self.device_view.model().delete(rows) + for path in paths: + self.status("Deleting "+path[path.rfind("/")+1:]) + self.dev.del_file(path, end_session=False) + fix_ids(self.reader_model.booklist, self.card_model.booklist) + self.status("Syncing media list to reader") + self.dev.upload_book_list(self.reader_model.booklist) + if len(self.card_model.booklist): + self.status("Syncing media list to card") + self.dev.upload_book_list(self.card_model.booklist) + self.update_availabe_space() + self.show_book(self.current_view.currentIndex(), QModelIndex()) + self.window.setCursor(Qt.ArrowCursor) - # Connect actions - QObject.connect(self.action_add, SIGNAL("triggered(bool)"), self.add) - QObject.connect(self.action_del, SIGNAL("triggered(bool)"), self.delete) - QObject.connect(self.action_edit, SIGNAL("triggered(bool)"), self.edit) + def read_settings(self): + settings = QSettings() + settings.beginGroup("MainWindow") + self.window.resize(settings.value("size", QVariant(QSize(1000, 700))).\ + toSize()) + settings.endGroup() + self.database_path = settings.value("database path", QVariant(os.path\ + .expanduser("~/library.db"))).toString() - # DnD setup - QObject.connect(self.book_cover, SIGNAL("cover_received(QPixmap)"), self.update_cover) + def write_settings(self): + settings = QSettings() + settings.beginGroup("MainWindow") + settings.setValue("size", QVariant(self.window.size())) + settings.endGroup() - self.detector = DeviceConnectDetector(self.dev) - self.connect(self.detector, SIGNAL("device_connected()"), self.establish_connection) - self.connect(self.detector, SIGNAL("device_removed()"), self.device_removed) - self.search.setFocus(Qt.OtherFocusReason) - self.show_device(False) - self.df_template = self.df.text() - self.df.setText(self.df_template.arg("").arg("").arg("")) - window.show() + def close_event(self, e): + self.write_settings() + e.accept() - def device_removed(self): - """ @todo: only reset stuff if library is not shown """ - self.df.setText(self.df_template.arg("").arg("").arg("")) - self.device_tree.hide_reader(True) - self.device_tree.hide_card(True) - self.book_cover.hide() - self.book_info.hide() - self.device_view.hide() - self.library_view.show() - - def progress(self, val): - if val < 0: - self.progress_bar.setMaximum(0) - else: self.progress_bar.setValue(val) - QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) - - def status(self, msg): - self.progress_bar.setMaximum(100) - self.progress_bar.reset() - self.progress_bar.setFormat(msg + ": %p%") - self.progress(0) - QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) - - def establish_connection(self): - self.window.setCursor(Qt.WaitCursor) - self.status("Connecting to device") - try: - info = self.dev.get_device_information(end_session=False) - except DeviceBusy, e: - qFatal(str(e)) - except DeviceError: - self.dev.reconnect() - self.detector.connection_failed() - return - except ProtocolError, e: - traceback.print_exc(e) - qFatal("Unable to connect to device. Please try unplugging and reconnecting it") - self.df.setText(self.df_template.arg("Connected: "+info[0]).arg(info[1]).arg(info[2])) - self.update_availabe_space(end_session=False) - self.card = self.dev.card() - self.is_connected = True - if self.card: self.device_tree.hide_card(False) - else: self.device_tree.hide_card(True) - self.device_tree.hide_reader(False) - self.status("Loading media list from SONY Reader") - self.reader_model.set_data(self.dev.books(end_session=False)) - if self.card: self.status("Loading media list from Storage Card") - self.card_model.set_data(self.dev.books(oncard=True)) - self.progress(100) - self.window.setCursor(Qt.ArrowCursor) + def add(self, action): + settings = QSettings() + _dir = settings.value("add books dialog dir", \ + QVariant(os.path.expanduser("~"))).toString() + files = QFileDialog.getOpenFileNames(self.window, \ + "Choose books to add to library", _dir, \ + "Books (*.lrf *.lrx *.rtf *.pdf *.txt);;All files (*)") + if not files.isEmpty(): + x = str(files[0]) + settings.setValue("add books dialog dir", QVariant(os.path.dirname(x))) + files = str(files.join("|||")).split("|||") + self.add_books(files) - def update_availabe_space(self, end_session=True): - space = self.dev.free_space(end_session=end_session) - sc = space[1] if int(space[1])>0 else space[2] - self.device_tree.model().update_free_space(space[0], sc) + def add_books(self, files): + self.window.setCursor(Qt.WaitCursor) + for _file in files: + _file = os.path.abspath(_file) + self.library_view.model().add_book(_file) + if self.library_view.isVisible(): self.search.clear() + else: self.library_model.search("") + hv = self.library_view.horizontalHeader() + col = hv.sortIndicatorSection() + order = hv.sortIndicatorOrder() + self.library_view.model().sort(col, order) + self.window.setCursor(Qt.ArrowCursor) -class LockFile(object): - def __init__(self, path): - self.path = path - f =open(path, "w") - f.close() - def __del__(self): - if os.access(self.path, os.F_OK): os.remove(self.path) + def edit(self, action): + if self.library_view.isVisible(): + rows = self.library_view.selectionModel().selectedRows() + for row in rows: + _id = self.library_model.id_from_index(row) + dialog = QDialog(self.window) + EditBookDialog(dialog, _id, self.library_model.db) + if dialog.exec_() == QDialog.Accepted: + self.library_model.refresh_row(row.row()) -class DeviceConnectDetector(QObject): - - def timerEvent(self, e): - if e.timerId() == self.device_detector: - is_connected = self.dev.is_connected() - if is_connected and not self.is_connected: - self.emit(SIGNAL("device_connected()")) - self.is_connected = True - elif not is_connected and self.is_connected: - self.emit(SIGNAL("device_removed()")) - self.is_connected = False + + 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: Error("Unable to change cover", e) + + def upload_books(self, to, files, ids): + oncard = False if to == "reader" else True + booklists = (self.reader_model.booklist, self.card_model.booklist) + def update_models(): + hv = self.device_view.horizontalHeader() + col = hv.sortIndicatorSection() + order = hv.sortIndicatorOrder() + model = self.card_model if oncard else self.reader_model + model.sort(col, order) + if self.device_view.isVisible() and self.device_view.model()\ + == model: self.search.clear() + else: model.search("") - def connection_failed(self): - # TODO: Do something intelligent if we're using HAL - self.is_connected = False - - def udi_is_device(self, udi): - ans = False - try: - devobj = bus.get_object('org.freedesktop.Hal', udi) - dev = dbus.Interface(devobj, "org.freedesktop.Hal.Device") - properties = dev.GetAllProperties() - vendor_id, product_id = int(properties["usb_device.vendor_id"]), int(properties["usb_device.product_id"]) - if self.dev.signature() == (vendor_id, product_id): ans = True - except: - self.device_detector = self.startTimer(1000) - return ans - - def device_added_callback(self, udi): - if self.udi_is_device(udi): - self.emit(SIGNAL("device_connected()")) - - def device_removed_callback(self, udi): - if self.udi_is_device(udi): - self.emit(SIGNAL("device_removed()")) - - def __init__(self, dev): - QObject.__init__(self) - self.dev = dev - try: - raise Exception("DBUS doesn't support the Qt mainloop") - import dbus - bus = dbus.SystemBus() - hal_manager_obj = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager') - hal_manager = dbus.Interface(hal_manager_obj, 'org.freedesktop.Hal.Manager') - hal_manager.connect_to_signal('DeviceAdded', self.device_added_callback) - hal_manager.connect_to_signal('DeviceRemoved', self.device_removed_callback) - except Exception, e: - #Warning("Could not connect to HAL", e) - self.is_connected = False - self.device_detector = self.startTimer(1000) + def sync_lists(): + self.status("Syncing media list to device main memory") + self.dev.upload_book_list(booklists[0]) + if len(booklists[1]): + self.status("Syncing media list to storage card") + self.dev.upload_book_list(booklists[1]) + + self.window.setCursor(Qt.WaitCursor) + ename = "file" + try: + if ids: + for _id in ids: + formats = [] + info = self.library_view.model().book_info(_id) + if info["cover"]: + pix = QPixmap() + pix.loadFromData(str(info["cover"])) + if pix.isNull(): + pix = DEFAULT_BOOK_COVER + pix = pix.scaledToHeight(self.dev.THUMBNAIL_HEIGHT, \ + Qt.SmoothTransformation) + _buffer = QBuffer() + _buffer.open(QIODevice.WriteOnly) + pix.save(_buffer, "JPEG") + info["cover"] = (pix.width(), pix.height(), \ + str(_buffer.buffer())) + ename = info["title"] + for f in files: + if re.match("......_"+str(_id)+"_", os.path.basename(f)): + formats.append(f) + _file = None + try: + for format in self.dev.FORMATS: + for f in formats: + if extension(f) == format: + _file = f + raise StopIteration() + except StopIteration: pass + if not _file: + Error("The library does not have any formats that "+\ + "can be viewed on the device for " + ename, None) + continue + f = open(_file, "rb") + self.status("Sending "+info["title"]+" to device") + try: + self.dev.add_book(f, "libprs500_"+str(_id)+"."+\ + extension(_file), info, booklists, oncard=oncard, \ + end_session=False) + update_models() + except PathError, e: + if "already exists" in str(e): + Error(info["title"] + \ + " already exists on the device", None) + self.progress(100) + continue + else: raise + finally: f.close() + sync_lists() + else: + for _file in files: + ename = _file + if extension(_file) not in self.dev.FORMATS: + Error(ename + " is not in a supported format") + continue + info = { "title":os.path.basename(_file), \ + "authors":"Unknown", "cover":(None, None, None) } + f = open(_file, "rb") + self.status("Sending "+info["title"]+" to device") + try: + self.dev.add_book(f, os.path.basename(_file), info, \ + booklists, oncard=oncard, end_session=False) + update_models() + except PathError, e: + if "already exists" in str(e): + Error(info["title"] + \ + " already exists on the device", None) + self.progress(100) + continue + else: raise + finally: f.close() + sync_lists() + except Exception, e: + Error("Unable to send "+ename+" to device", e) + finally: + self.window.setCursor(Qt.ArrowCursor) + self.update_availabe_space() + + def __init__(self, window, log_packets): + QObject.__init__(self) + Ui_MainWindow.__init__(self) + + self.dev = device(report_progress=self.progress, log_packets=log_packets) + self.setupUi(window) + self.card = None + self.window = window + window.closeEvent = self.close_event + self.read_settings() + + # Setup Library Book list + self.library_model = LibraryBooksModel(window) + self.library_model.set_data(LibraryDatabase(str(self.database_path))) + self.library_view.setModel(self.library_model) + self.current_view = self.library_view + QObject.connect(self.library_model, SIGNAL("layoutChanged()"), \ + self.library_view.resizeRowsToContents) + QObject.connect(self.library_view.selectionModel(), \ + SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) + QObject.connect(self.search, SIGNAL("textChanged(QString)"), \ + self.library_model.search) + QObject.connect(self.library_model, SIGNAL("sorted()"), self.model_modified) + QObject.connect(self.library_model, SIGNAL("searched()"), \ + self.model_modified) + QObject.connect(self.library_model, SIGNAL("deleted()"), \ + self.model_modified) + QObject.connect(self.library_model, \ + SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns) + QObject.connect(self.library_view, \ + SIGNAL('books_dropped'), self.add_books) + QObject.connect(self.library_model, \ + SIGNAL('formats_added'), self.formats_added) + self.library_view.resizeColumnsToContents() + + # Create Device tree + model = DeviceModel(self.device_tree) + QObject.connect(self.device_tree, SIGNAL("activated(QModelIndex)"), \ + self.tree_clicked) + QObject.connect(self.device_tree, SIGNAL("clicked(QModelIndex)"), \ + self.tree_clicked) + QObject.connect(model, SIGNAL('books_dropped'), self.add_books) + QObject.connect(model, SIGNAL('upload_books'), self.upload_books) + self.device_tree.setModel(model) + + # Create Device Book list + self.reader_model = DeviceBooksModel(window) + self.card_model = DeviceBooksModel(window) + self.device_view.setModel(self.reader_model) + QObject.connect(self.device_view.selectionModel(), \ + SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) + for model in (self.reader_model, self. card_model): + QObject.connect(model, SIGNAL("layoutChanged()"), \ + self.device_view.resizeRowsToContents) + QObject.connect(self.search, SIGNAL("textChanged(QString)"), \ + model.search) + QObject.connect(model, SIGNAL("sorted()"), self.model_modified) + QObject.connect(model, SIGNAL("searched()"), self.model_modified) + QObject.connect(model, SIGNAL("deleted()"), self.model_modified) + QObject.connect(model, SIGNAL("dataChanged(QModelIndex, QModelIndex)")\ + , self.resize_columns) + + # Setup book display + self.book_cover.hide() + self.book_info.hide() + + # Connect actions + QObject.connect(self.action_add, SIGNAL("triggered(bool)"), self.add) + QObject.connect(self.action_del, SIGNAL("triggered(bool)"), self.delete) + QObject.connect(self.action_edit, SIGNAL("triggered(bool)"), self.edit) + + # DnD setup + QObject.connect(self.book_cover, SIGNAL("cover_received(QPixmap)"), \ + self.update_cover) + + self.detector = DeviceConnectDetector(self.dev) + self.connect(self.detector, SIGNAL("device_connected()"), \ + self.establish_connection) + self.connect(self.detector, SIGNAL("device_removed()"), self.device_removed) + self.search.setFocus(Qt.OtherFocusReason) + self.show_device(False) + self.df_template = self.df.text() + self.df.setText(self.df_template.arg("").arg("").arg("")) + window.show() + + def device_removed(self): + """ @todo: only reset stuff if library is not shown """ + self.df.setText(self.df_template.arg("").arg("").arg("")) + self.device_tree.hide_reader(True) + self.device_tree.hide_card(True) + self.book_cover.hide() + self.book_info.hide() + self.device_view.hide() + self.library_view.show() + + def progress(self, val): + if val < 0: + self.progress_bar.setMaximum(0) + else: self.progress_bar.setValue(val) + QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) + + def status(self, msg): + self.progress_bar.setMaximum(100) + self.progress_bar.reset() + self.progress_bar.setFormat(msg + ": %p%") + self.progress(0) + QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) + + def establish_connection(self): + self.window.setCursor(Qt.WaitCursor) + self.status("Connecting to device") + try: + info = self.dev.get_device_information(end_session=False) + except DeviceBusy, e: + qFatal(str(e)) + except DeviceError: + self.dev.reconnect() + self.detector.connection_failed() + return + except ProtocolError, e: + traceback.print_exc(e) + qFatal("Unable to connect to device. Please try unplugging and"+\ + " reconnecting it") + self.df.setText(self.df_template.arg("Connected: "+info[0])\ + .arg(info[1]).arg(info[2])) + self.update_availabe_space(end_session=False) + self.card = self.dev.card() + self.is_connected = True + if self.card: self.device_tree.hide_card(False) + else: self.device_tree.hide_card(True) + self.device_tree.hide_reader(False) + self.status("Loading media list from SONY Reader") + self.reader_model.set_data(self.dev.books(end_session=False)) + if self.card: self.status("Loading media list from Storage Card") + self.card_model.set_data(self.dev.books(oncard=True)) + self.progress(100) + self.window.setCursor(Qt.ArrowCursor) + + def update_availabe_space(self, end_session=True): + space = self.dev.free_space(end_session=end_session) + sc = space[1] if int(space[1])>0 else space[2] + self.device_tree.model().update_free_space(space[0], sc) + +class LockFile(object): + def __init__(self, path): + self.path = path + f = open(path, "w") + f.close() + + def __del__(self): + if os.access(self.path, os.F_OK): os.remove(self.path) + +class DeviceConnectDetector(QObject): + + def timerEvent(self, e): + if e.timerId() == self.device_detector: + is_connected = self.dev.is_connected() + if is_connected and not self.is_connected: + self.emit(SIGNAL("device_connected()")) + self.is_connected = True + elif not is_connected and self.is_connected: + self.emit(SIGNAL("device_removed()")) + self.is_connected = False + + def connection_failed(self): + # TODO: Do something intelligent if we're using HAL + self.is_connected = False + + def udi_is_device(self, udi): + ans = False + try: + devobj = bus.get_object('org.freedesktop.Hal', udi) + dev = dbus.Interface(devobj, "org.freedesktop.Hal.Device") + properties = dev.GetAllProperties() + vendor_id = int(properties["usb_device.vendor_id"]), + product_id = int(properties["usb_device.product_id"]) + if self.dev.signature() == (vendor_id, product_id): ans = True + except: + self.device_detector = self.startTimer(1000) + return ans + + def device_added_callback(self, udi): + if self.udi_is_device(udi): + self.emit(SIGNAL("device_connected()")) + + def device_removed_callback(self, udi): + if self.udi_is_device(udi): + self.emit(SIGNAL("device_removed()")) + + def __init__(self, dev): + QObject.__init__(self) + self.dev = dev + try: + raise Exception("DBUS doesn't support the Qt mainloop") + import dbus + bus = dbus.SystemBus() + hal_manager_obj = bus.get_object('org.freedesktop.Hal',\ + '/org/freedesktop/Hal/Manager') + hal_manager = dbus.Interface(hal_manager_obj,\ + 'org.freedesktop.Hal.Manager') + hal_manager.connect_to_signal('DeviceAdded', \ + self.device_added_callback) + hal_manager.connect_to_signal('DeviceRemoved', \ + self.device_removed_callback) + except Exception, e: + #_Warning("Could not connect to HAL", e) + self.is_connected = False + self.device_detector = self.startTimer(1000) def main(): from optparse import OptionParser from libprs500 import __version__ as VERSION lock = os.path.join(tempfile.gettempdir(),"libprs500_gui_lock") if os.access(lock, os.F_OK): - print >>sys.stderr, "Another instance of", APP_TITLE, "is running" - print >>sys.stderr, "If you are sure this is not the case then manually delete the file", lock - sys.exit(1) + print >>sys.stderr, "Another instance of", APP_TITLE, "is running" + print >>sys.stderr, "If you are sure this is not the case then "+\ + "manually delete the file", lock + sys.exit(1) parser = OptionParser(usage="usage: %prog [options]", version=VERSION) parser.add_option("--log-packets", help="print out packet stream to stdout. "+\ - "The numbers in the left column are byte offsets that allow the packet size to be read off easily.", \ + "The numbers in the left column are byte offsets that allow"+\ + " the packet size to be read off easily.", \ dest="log_packets", action="store_true", default=False) options, args = parser.parse_args() from PyQt4.Qt import QApplication, QMainWindow @@ -483,9 +558,9 @@ def main(): installErrorHandler(QErrorMessage(window)) QCoreApplication.setOrganizationName("KovidsBrain") QCoreApplication.setApplicationName(APP_TITLE) - gui = MainWindow(window, options.log_packets) + Main(window, options.log_packets) lock = LockFile(lock) return app.exec_() - + if __name__ == "__main__": sys.exit(main()) diff --git a/libprs500/gui/widgets.py b/libprs500/gui/widgets.py index 68d19f13c5..ebe67e3b75 100644 --- a/libprs500/gui/widgets.py +++ b/libprs500/gui/widgets.py @@ -12,7 +12,12 @@ ## 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. -import re, os, string, textwrap, time, traceback, sys +import re +import os +import textwrap +import time +import traceback +import sys from operator import itemgetter, attrgetter from socket import gethostname from urlparse import urlparse, urlunparse @@ -20,732 +25,821 @@ 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 -from libprs500.gui import Error, Warning +from libprs500.gui import Error, _Warning from PyQt4 import QtGui, QtCore from PyQt4.QtCore import Qt, SIGNAL -from PyQt4.Qt import QApplication, QString, QFont, QAbstractListModel, QVariant, QAbstractTableModel, QTableView, QListView, QLabel,\ - QAbstractItemView, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog, QSpinBox, QPoint, QTemporaryFile, QDir, QFile, QIODevice,\ - QPainterPath, QItemDelegate, QPainter, QPen, QColor, QLinearGradient, QBrush, QStyle,\ - QStringList, QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, QDrag, QRect +from PyQt4.Qt import QApplication, QString, QFont, QAbstractListModel, \ + QVariant, QAbstractTableModel, QTableView, QListView, \ + QLabel, QAbstractItemView, QPixmap, QIcon, QSize, \ + QMessageBox, QSettings, QFileDialog, QErrorMessage, \ + QSpinBox, QPoint, QTemporaryFile, QDir, QFile, \ + QIODevice, QPainterPath, QItemDelegate, QPainter, QPen, \ + QColor, QLinearGradient, QBrush, QStyle, QStringList, \ + QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, \ + QDrag, QRect NONE = QVariant() #: Null value to return from the data function of item models TIME_WRITE_FMT = "%d %b %Y" #: The display format used to show dates class FileDragAndDrop(object): - _drag_start_position = QPoint() - _dragged_files = [] - - @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 and o.scheme != 'file': - Warning(o.scheme + " not supported in drop events", None) - continue - path = unquote(o.path) - if not os.access(path, os.R_OK): - Warning("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 + _drag_start_position = QPoint() + _dragged_files = [] - def __init__(self, QtBaseClass, enable_drag=True): - self.QtBaseClass = QtBaseClass - self.enable_drag = enable_drag - - def mousePressEvent(self, event): - self.QtBaseClass.mousePressEvent(self, event) - if self.enable_drag: - if event.button == Qt.LeftButton: - self._drag_start_position = event.pos() - - - def mouseMoveEvent(self, event): - self.QtBaseClass.mousePressEvent(self, event) - if self.enable_drag: - 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) + @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): + """ + Return list of paths from event that point to files to + which the user has read permission. + """ + 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 and o.scheme != 'file': + _Warning(o.scheme + " not supported in drop events", None) + continue + path = unquote(o.path) + if not os.access(path, os.R_OK): + _Warning("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 __init__(self, QtBaseClass, enable_drag=True): + self.QtBaseClass = QtBaseClass + self.enable_drag = enable_drag + + def mousePressEvent(self, event): + self.QtBaseClass.mousePressEvent(self, event) + if self.enable_drag: + if event.button == Qt.LeftButton: + self._drag_start_position = event.pos() - 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: - try: - event.setDropAction(Qt.CopyAction) - if self.files_dropped(files, event): event.accept() - except Exception, e: - Error("There was an error processing the dropped files.", e) - raise e - - - def files_dropped(self, files, event): return False - - def drag_object_from_files(self, files): - if files: - drag = QDrag(self) - mime_data = QMimeData() - self._dragged_files, urls = [], [] - for file in files: - urls.append(urlunparse(('file', quote(gethostname()), quote(str(file.name)), '','',''))) - self._dragged_files.append(file) - mime_data.setData("text/uri-list", QByteArray("\n".join(urls))) - user = os.getenv('USER') - if user: mime_data.setData("text/x-xdnd-username", QByteArray(user)) - drag.setMimeData(mime_data) - return drag - - def drag_object(self, extensions): - if extensions: - files = [] - for ext in extensions: - f = TemporaryFile(ext=ext) - f.open() - files.append(f) - return self.drag_object_from_files(files), self._dragged_files - - + def mouseMoveEvent(self, event): + self.QtBaseClass.mousePressEvent(self, event) + if self.enable_drag: + 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): + raise NotImplementedError() + + 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: + try: + event.setDropAction(Qt.CopyAction) + if self.files_dropped(files, event): + event.accept() + except Exception, e: + Error("There was an error processing the dropped files.", e) + raise e + + + def files_dropped(self, files, event): + raise NotImplementedError() + + def drag_object_from_files(self, files): + if files: + drag = QDrag(self) + mime_data = QMimeData() + self._dragged_files, urls = [], [] + for _file in files: + urls.append(urlunparse(('file', quote(gethostname()), \ + quote(str(_file.name)), '','',''))) + self._dragged_files.append(_file) + mime_data.setData("text/uri-list", QByteArray("\n".join(urls))) + user = os.getenv('USER') + if user: + mime_data.setData("text/x-xdnd-username", QByteArray(user)) + drag.setMimeData(mime_data) + return drag + + def drag_object(self, extensions): + if extensions: + files = [] + for ext in extensions: + f = TemporaryFile(ext=ext) + f.open() + files.append(f) + return self.drag_object_from_files(files), self._dragged_files + + class TableView(FileDragAndDrop, QTableView): - def __init__(self, parent): - FileDragAndDrop.__init__(self, QTableView) - QTableView.__init__(self, parent) + def __init__(self, parent): + FileDragAndDrop.__init__(self, QTableView) + QTableView.__init__(self, parent) - @classmethod - def wrap(cls, s, width=20): return textwrap.fill(str(s), width) - - @classmethod - def human_readable(cls, 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 render_to_pixmap(self, indices): - rect = self.visualRect(indices[0]) - rects = [] - for i in range(len(indices)): - rects.append(self.visualRect(indices[i])) - rect |= rects[i] - rect = rect.intersected(self.viewport().rect()) - pixmap = QPixmap(rect.size()) - pixmap.fill(self.palette().base().color()) - painter = QPainter(pixmap) - option = self.viewOptions() - option.state |= QStyle.State_Selected - for j in range(len(indices)): - option.rect = QRect(rects[j].topLeft() - rect.topLeft(), rects[j].size()) - self.itemDelegate(indices[j]).paint(painter, option, indices[j]) - painter.end() - return pixmap + @classmethod + def wrap(cls, s, width=20): + return textwrap.fill(str(s), width) - def drag_object_from_files(self, files): - drag = FileDragAndDrop.drag_object_from_files(self, files) - drag.setPixmap(self.render_to_pixmap(self.selectedIndexes())) - return drag - + @classmethod + def human_readable(cls, 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 render_to_pixmap(self, indices): + rect = self.visualRect(indices[0]) + rects = [] + for i in range(len(indices)): + rects.append(self.visualRect(indices[i])) + rect |= rects[i] + rect = rect.intersected(self.viewport().rect()) + pixmap = QPixmap(rect.size()) + pixmap.fill(self.palette().base().color()) + painter = QPainter(pixmap) + option = self.viewOptions() + option.state |= QStyle.State_Selected + for j in range(len(indices)): + option.rect = QRect(rects[j].topLeft() - rect.topLeft(), \ + rects[j].size()) + self.itemDelegate(indices[j]).paint(painter, option, indices[j]) + painter.end() + return pixmap + + def drag_object_from_files(self, files): + drag = FileDragAndDrop.drag_object_from_files(self, files) + drag.setPixmap(self.render_to_pixmap(self.selectedIndexes())) + return drag + class TemporaryFile(QTemporaryFile): - _file_name = "" - def __del__(self): - if os.access(self.name, os.F_OK): os.remove(self.name) - def __init__(self, ext=""): - if ext: ext = "." + ext - path = QDir.tempPath() + "/" + TFT + "_XXXXXX"+ext - QTemporaryFile.__init__(self, path) + _file_name = "" + def __del__(self): + if os.access(self.name, os.F_OK): os.remove(self.name) + def __init__(self, ext=""): + if ext: ext = "." + ext + path = QDir.tempPath() + "/" + TFT + "_XXXXXX"+ext + QTemporaryFile.__init__(self, path) - def open(self): - ok = QFile.open(self, QIODevice.ReadWrite) - self._file_name = os.path.normpath(os.path.abspath(str(QTemporaryFile.fileName(self)))) - return ok - - @apply - def name(): - def fget(self): - return self._file_name - return property(**locals()) + def open(self): + ok = QFile.open(self, QIODevice.ReadWrite) + self._file_name = os.path.normpath(os.path.abspath(\ + str(QTemporaryFile.fileName(self)))) + return ok + @apply + def name(): + def fget(self): + return self._file_name + return property(**locals()) + class NamedTemporaryFile(TemporaryFile): - def __init__(self, name): - path = QDir.tempPath() + "/" + "XXXXXX"+name - QTemporaryFile.__init__(self, path) + def __init__(self, name): + path = QDir.tempPath() + "/" + "XXXXXX"+name + QTemporaryFile.__init__(self, path) class CoverDisplay(FileDragAndDrop, QLabel): - def __init__(self, parent): - FileDragAndDrop.__init__(self, QLabel) - QLabel.__init__(self, parent) - def files_dropped(self, files, event): - 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) + def __init__(self, parent): + FileDragAndDrop.__init__(self, QLabel) + QLabel.__init__(self, parent) + def files_dropped(self, files, event): + 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(FileDragAndDrop, QListView): - def __init__(self, parent): - FileDragAndDrop.__init__(self, QListView, enable_drag=False) - QListView.__init__(self, parent) + def __init__(self, parent): + FileDragAndDrop.__init__(self, QListView, enable_drag=False) + QListView.__init__(self, parent) - def hide_reader(self, x): - self.model().update_devices(reader=not x) + def hide_reader(self, x): + self.model().update_devices(reader=not x) - def hide_card(self, x): - self.model().update_devices(card=not x) + def hide_card(self, x): + self.model().update_devices(card=not x) - def files_dropped(self, files, event): - ids = [] - md = event.mimeData() - if md.hasFormat("application/x-libprs500-id"): - ids = [ int(id) for id in FileDragAndDrop._bytes_to_string(md.data("application/x-libprs500-id")).split()] - index = self.indexAt(event.pos()) - if index.isValid(): - return self.model().files_dropped(files, index, ids) + def files_dropped(self, files, event): + ids = [] + md = event.mimeData() + if md.hasFormat("application/x-libprs500-id"): + ids = [ int(id) for id in FileDragAndDrop._bytes_to_string(\ + md.data("application/x-libprs500-id")).split()] + index = self.indexAt(event.pos()) + if index.isValid(): + return self.model().files_dropped(files, index, ids) class DeviceBooksView(TableView): - def __init__(self, parent): - TableView.__init__(self, parent) - self.setSelectionBehavior(QAbstractItemView.SelectRows) - self.setSortingEnabled(True) + def __init__(self, parent): + TableView.__init__(self, parent) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setSortingEnabled(True) class LibraryBooksView(TableView): - def __init__(self, parent): - TableView.__init__(self, parent) - self.setSelectionBehavior(QAbstractItemView.SelectRows) - self.setSortingEnabled(True) - self.setItemDelegate(LibraryDelegate(self, rating_column=4)) + def __init__(self, parent): + TableView.__init__(self, parent) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setSortingEnabled(True) + self.setItemDelegate(LibraryDelegate(self, rating_column=4)) - def dragEnterEvent(self, event): - if not event.mimeData().hasFormat("application/x-libprs500-id"): - FileDragAndDrop.dragEnterEvent(self, event) - - - def start_drag(self, pos): - index = self.indexAt(pos) - if index.isValid(): - rows = frozenset([ index.row() for index in self.selectedIndexes() ]) - files = self.model().extract_formats(rows) - drag = self.drag_object_from_files(files) - if drag: - ids = [ str(self.model().id_from_row(row)) for row in rows ] - drag.mimeData().setData("application/x-libprs500-id", QByteArray("\n".join(ids))) - drag.start() - - - def files_dropped(self, files, event): - if not files: return - index = self.indexAt(event.pos()) - if index.isValid(): - self.model().add_formats(files, index) - else: self.emit(SIGNAL('books_dropped'), files) - - + def dragEnterEvent(self, event): + if not event.mimeData().hasFormat("application/x-libprs500-id"): + FileDragAndDrop.dragEnterEvent(self, event) + + + def start_drag(self, pos): + index = self.indexAt(pos) + if index.isValid(): + rows = frozenset([ index.row() for index in self.selectedIndexes()]) + files = self.model().extract_formats(rows) + drag = self.drag_object_from_files(files) + if drag: + ids = [ str(self.model().id_from_row(row)) for row in rows ] + drag.mimeData().setData("application/x-libprs500-id", \ + QByteArray("\n".join(ids))) + drag.start() + + + def files_dropped(self, files, event): + if not files: return + index = self.indexAt(event.pos()) + if index.isValid(): + self.model().add_formats(files, index) + else: self.emit(SIGNAL('books_dropped'), files) + + class LibraryDelegate(QItemDelegate): - COLOR = QColor("blue") - SIZE = 16 - PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) - - def __init__(self, parent, rating_column=-1): - QItemDelegate.__init__(self, parent ) - self.rating_column = rating_column - 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. + COLOR = QColor("blue") + SIZE = 16 + PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) + + def __init__(self, parent, rating_column=-1): + QItemDelegate.__init__(self, parent ) + self.rating_column = rating_column + 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() != self.rating_column: - 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() != self.rating_column: - 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 sizeHint(self, option, index): + if index.column() != self.rating_column: + return QItemDelegate.sizeHint(self, option, index) + num = index.model().data(index, Qt.DisplayRole).toInt()[0] + return QSize(num*(self.SIZE), self.SIZE+4) - 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 paint(self, painter, option, index): + if index.column() != self.rating_column: + 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 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 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 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 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 updateEditorGeometry(self, editor, option, index): - if index.column() != 4: - return QItemDelegate.updateEditorGeometry(self, editor, option, index) - editor.setGeometry(option.rect) + 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", "comments"] - 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 + FIELDS = ["id", "title", "authors", "size", "date", "rating", "publisher", \ + "tags", "comments"] + 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 - def extract_formats(self, rows): - files = [] - for row in rows: - id = self.id_from_row(row) - au = self._data[row]["authors"] if self._data[row]["authors"] else "Unknown" - basename = re.sub("\n", "", "_"+str(id)+"_"+self._data[row]["title"]+" by "+ au) - exts = self.db.get_extensions(id) - for ext in exts: - fmt = self.db.get_format(id, ext) - if not ext: ext ="" - else: ext = "."+ext - name = basename+ext - file = NamedTemporaryFile(name) - file.open() - if not fmt: continue - file.write(QByteArray(fmt)) - file.close() - files.append(file) - return files - - 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 add_formats(self, paths, index): - for path in paths: - 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() - self.emit(SIGNAL('formats_added'), index) + def extract_formats(self, rows): + files = [] + for row in rows: + _id = self.id_from_row(row) + au = self._data[row]["authors"] if self._data[row]["authors"] \ + else "Unknown" + basename = re.sub("\n", "", "_"+str(_id)+"_"+\ + self._data[row]["title"]+" by "+ au) + exts = self.db.get_extensions(_id) + for ext in exts: + fmt = self.db.get_format(_id, ext) + if not ext: + ext ="" + else: + ext = "."+ext + name = basename+ext + file = NamedTemporaryFile(name) + file.open() + if not fmt: + continue + file.write(QByteArray(fmt)) + file.close() + files.append(file) + return files - def rowCount(self, parent): return len(self._data) - def columnCount(self, parent): return len(self.FIELDS)-3 + 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 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(): - if index.column() not in [2,3]: flags |= Qt.ItemIsEditable - 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 add_formats(self, paths, index): + for path in paths: + 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() + self.emit(SIGNAL('formats_added'), index) - 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 rowCount(self, parent): + return len(self._data) + + def columnCount(self, parent): + return len(self.FIELDS)-3 - 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 - tags = row["tags"] - if not tags: tags = "" - comments = row["comments"] - if not comments: comments = "" - return exts, tags, comments, cover - - def id_from_index(self, index): return self._data[index.row()]["id"] - def id_from_row(self, row): return self._data[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 book_info(self, id): - """ Return title, authors and cover in a dict """ - cover = self.db.get_cover(id) - info = self.db.get_row_by_id(id, ["title", "authors"]) - info["cover"] = cover - return info - - 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 = TableView.wrap(row["title"], width=25) - elif col == 1: - au = row["authors"] - if au : text = TableView.wrap(re.sub("&", "\n", au), width=25) - elif col == 2: text = TableView.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 = TableView.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 | Qt.AlignVCenter) - 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 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 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 flags(self, index): + flags = QAbstractTableModel.flags(self, index) + if index.isValid(): + if index.column() not in [2, 3]: + flags |= Qt.ItemIsEditable + 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 + tags = row["tags"] + if not tags: tags = "" + comments = row["comments"] + if not comments: comments = "" + return exts, tags, comments, cover + + def id_from_index(self, index): return self._data[index.row()]["id"] + def id_from_row(self, row): return self._data[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 book_info(self, _id): + """ Return title, authors and cover in a dict """ + cover = self.db.get_cover(_id) + info = self.db.get_row_by_id(_id, ["title", "authors"]) + info["cover"] = cover + return info + + 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 = TableView.wrap(row["title"], width=25) + elif col == 1: + au = row["authors"] + if au: + text = TableView.wrap(re.sub("&", "\n", au), width=25) + elif col == 2: + text = TableView.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 = TableView.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 | Qt.AlignVCenter) + 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", lambda x : x.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 on this models view after this """ + _id = self.db.add_book(path) + self._orig_data.append(self.db.get_row_by_id(_id, self.FIELDS)) - def add_book(self, path): - """ Must call search and sort on this models view after this """ - id = self.db.add_book(path) - self._orig_data.append(self.db.get_row_by_id(id, self.FIELDS)) - class DeviceBooksModel(QAbstractTableModel): - @apply - def booklist(): - doc = """ The booklist this model is based on """ - def fget(self): - return self._orig_data - return property(**locals()) + @apply + def booklist(): + doc = """ The booklist this model is based on """ + def fget(self): + return self._orig_data + return property(doc=doc, fget=fget) - def __init__(self, parent): - QAbstractTableModel.__init__(self, parent) - self._data = [] - self._orig_data = [] + 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 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 rowCount(self, parent): return len(self._data) + def columnCount(self, parent): return 4 - def data(self, index, role): - if role == Qt.DisplayRole: - row, col = index.row(), index.column() - book = self._data[row] - if col == 0: text = TableView.wrap(book.title, width=40) - elif col == 1: text = re.sub("&\s*","\n", book.author) - elif col == 2: text = TableView.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 | Qt.AlignVCenter) - return NONE + 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 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, TableView.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 data(self, index, role): + if role == Qt.DisplayRole: + row, col = index.row(), index.column() + book = self._data[row] + if col == 0: + text = TableView.wrap(book.title, width=40) + elif col == 1: + text = re.sub("&\s*", "\n", book.author) + elif col == 2: + text = TableView.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 | Qt.AlignVCenter) + 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, TableView.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", lambda x : x.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(self, indices): + paths = [] + rows = [ index.row() for index in indices ] + if not rows: + return + self.emit(SIGNAL("layoutAboutToBeChanged()")) + elems = [ self._data[row] for row in rows ] + for e in elems: + _id = e.id + paths.append(e.path) + self._orig_data.delete_book(_id) + try: + self._data.remove(e) + except ValueError: + pass + self.emit(SIGNAL("layoutChanged()")) + return paths + + def path(self, index): + return self._data[index.row()].path + def title(self, index): + return self._data[index.row()].title + - 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(self, indices): - paths = [] - rows = [ index.row() for index in indices ] - if not rows: return - self.emit(SIGNAL("layoutAboutToBeChanged()")) - elems = [ self._data[row] for row in rows ] - for e in elems: - id = e.id - paths.append(e.path) - self._orig_data.delete_book(id) - try: self._data.remove(e) - except ValueError: pass - self.emit(SIGNAL("layoutChanged()")) - return paths - - def path(self, index): return self._data[index.row()].path - def title(self, index): return self._data[index.row()].title - - class DeviceModel(QAbstractListModel): - - memory_free = 0 - card_free = 0 - show_reader = False - show_card = False - - def update_devices(self, reader=None, card=None): - if reader != None: self.show_reader = reader - if card != None: self.show_card = card - self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(1), self.index(2)) - - def rowCount(self, parent): return 3 - - def update_free_space(self, reader, card): - self.memory_free = reader - self.card_free = card - self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(1), self.index(2)) - - def data(self, index, role): - row = index.row() - data = NONE - if role == Qt.DisplayRole: - text = None - if row == 0: text = "Library" - if row == 1 and self.show_reader: - text = "Reader\n" + TableView.human_readable(self.memory_free) + " available" - elif row == 2 and self.show_card: - text = "Card\n" + TableView.human_readable(self.card_free) + " available" - if text: data = QVariant(text) - elif role == Qt.DecorationRole: - icon = None - if row == 0: icon = QIcon(":/library") - elif row == 1 and self.show_reader: icon = QIcon(":/reader") - elif self.show_card: icon = QIcon(":/card") - if icon: data = QVariant(icon) - elif role == Qt.SizeHintRole: - if row == 1: return QVariant(QSize(150, 70)) - elif role == Qt.FontRole: - font = QFont() - font.setBold(True) - data = QVariant(font) - return data - def is_library(self, index): return index.row() == 0 - def is_reader(self, index): return index.row() == 1 - def is_card(self, index): return index.row() == 2 - - def files_dropped(self, files, index, ids): - ret = False - if self.is_library(index) and not ids: - self.emit(SIGNAL("books_dropped"), files) - ret = True - elif self.is_reader(index): - self.emit(SIGNAL("upload_books"), "reader", files, ids) - elif self.is_card(index): - self.emit(SIGNAL("upload_books"), "card", files, ids) - return ret + memory_free = 0 + card_free = 0 + show_reader = False + show_card = False + + def update_devices(self, reader=None, card=None): + if reader != None: + self.show_reader = reader + if card != None: + self.show_card = card + self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ + self.index(1), self.index(2)) + + def rowCount(self, parent): return 3 + + def update_free_space(self, reader, card): + self.memory_free = reader + self.card_free = card + self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ + self.index(1), self.index(2)) + + def data(self, index, role): + row = index.row() + data = NONE + if role == Qt.DisplayRole: + text = None + if row == 0: + text = "Library" + if row == 1 and self.show_reader: + text = "Reader\n" + TableView.human_readable(self.memory_free) \ + + " available" + elif row == 2 and self.show_card: + text = "Card\n" + TableView.human_readable(self.card_free) \ + + " available" + if text: + data = QVariant(text) + elif role == Qt.DecorationRole: + icon = None + if row == 0: + icon = QIcon(":/library") + elif row == 1 and self.show_reader: + icon = QIcon(":/reader") + elif self.show_card: + icon = QIcon(":/card") + if icon: + data = QVariant(icon) + elif role == Qt.SizeHintRole: + if row == 1: + return QVariant(QSize(150, 70)) + elif role == Qt.FontRole: + font = QFont() + font.setBold(True) + data = QVariant(font) + return data + + def is_library(self, index): + return index.row() == 0 + def is_reader(self, index): + return index.row() == 1 + def is_card(self, index): + return index.row() == 2 + + def files_dropped(self, files, index, ids): + ret = False + if self.is_library(index) and not ids: + self.emit(SIGNAL("books_dropped"), files) + ret = True + elif self.is_reader(index): + self.emit(SIGNAL("upload_books"), "reader", files, ids) + elif self.is_card(index): + self.emit(SIGNAL("upload_books"), "card", files, ids) + return ret diff --git a/setup.py b/setup.py index 9a7214467b..21139504b3 100644 --- a/setup.py +++ b/setup.py @@ -89,5 +89,6 @@ try: except ImportError: print "You do not have PyQt4 installed. The GUI will not work. You can obtain PyQt4 from http://www.riverbankcomputing.co.uk/pyqt/download.php" else: + import PyQt4.Qt if PyQt4.Qt.PYQT_VERSION < 0x40101: print "WARNING: The GUI needs PyQt >= 4.1.1"