diff --git a/src/libprs500/ebooks/lrf/html/convert_from.py b/src/libprs500/ebooks/lrf/html/convert_from.py
index e3af458e72..df5ca537a5 100644
--- a/src/libprs500/ebooks/lrf/html/convert_from.py
+++ b/src/libprs500/ebooks/lrf/html/convert_from.py
@@ -233,8 +233,6 @@ class HTMLConverter(object):
(re.compile(r'page-break-before:\s*\w+([\s;\}])'),
lambda match: match.group(1)) ]
-
-
class Link(object):
def __init__(self, para, tag):
self.para = para
diff --git a/src/libprs500/gui2/Makefile b/src/libprs500/gui2/Makefile
new file mode 100644
index 0000000000..391673edf1
--- /dev/null
+++ b/src/libprs500/gui2/Makefile
@@ -0,0 +1,8 @@
+main_ui.py : main.ui
+ pyuic4 main.ui | sed -s s/^.*Margin.*// | sed -e s/^.*boxlayout\.setObjectName.*// > main_ui.py
+
+images_rc.py : images.qrc
+ pyrcc4 images.qrc > images_rc.py
+
+clean :
+ rm main_ui.py images_rc.py
diff --git a/src/libprs500/gui2/images.qrc b/src/libprs500/gui2/images.qrc
new file mode 100644
index 0000000000..8f70ed6682
--- /dev/null
+++ b/src/libprs500/gui2/images.qrc
@@ -0,0 +1,16 @@
+
+
+ images/addfile.png
+ images/cherubs.jpg
+ images/clear.png
+ images/delfile.png
+ images/edit.png
+ images/fileopen.png
+ images/library.png
+ images/memory_stick_unmount.png
+ images/minus.png
+ images/plus.png
+ images/upload.png
+ images/reader.png
+
+
diff --git a/src/libprs500/gui2/images/addfile.png b/src/libprs500/gui2/images/addfile.png
new file mode 100644
index 0000000000..c89f43c6ac
Binary files /dev/null and b/src/libprs500/gui2/images/addfile.png differ
diff --git a/src/libprs500/gui2/images/cherubs.jpg b/src/libprs500/gui2/images/cherubs.jpg
new file mode 100644
index 0000000000..9147589ba6
Binary files /dev/null and b/src/libprs500/gui2/images/cherubs.jpg differ
diff --git a/src/libprs500/gui2/images/clear.png b/src/libprs500/gui2/images/clear.png
new file mode 100644
index 0000000000..913ee8da45
Binary files /dev/null and b/src/libprs500/gui2/images/clear.png differ
diff --git a/src/libprs500/gui2/images/delfile.png b/src/libprs500/gui2/images/delfile.png
new file mode 100644
index 0000000000..5a2218617e
Binary files /dev/null and b/src/libprs500/gui2/images/delfile.png differ
diff --git a/src/libprs500/gui2/images/edit.png b/src/libprs500/gui2/images/edit.png
new file mode 100644
index 0000000000..b28e20a259
Binary files /dev/null and b/src/libprs500/gui2/images/edit.png differ
diff --git a/src/libprs500/gui2/images/fileopen.png b/src/libprs500/gui2/images/fileopen.png
new file mode 100644
index 0000000000..3f5b36c1eb
Binary files /dev/null and b/src/libprs500/gui2/images/fileopen.png differ
diff --git a/src/libprs500/gui2/images/library.png b/src/libprs500/gui2/images/library.png
new file mode 100644
index 0000000000..ee3f19baa6
Binary files /dev/null and b/src/libprs500/gui2/images/library.png differ
diff --git a/src/libprs500/gui2/images/memory_stick_unmount.png b/src/libprs500/gui2/images/memory_stick_unmount.png
new file mode 100644
index 0000000000..f22c757dc8
Binary files /dev/null and b/src/libprs500/gui2/images/memory_stick_unmount.png differ
diff --git a/src/libprs500/gui2/images/minus.png b/src/libprs500/gui2/images/minus.png
new file mode 100644
index 0000000000..0f68cb7580
Binary files /dev/null and b/src/libprs500/gui2/images/minus.png differ
diff --git a/src/libprs500/gui2/images/plus.png b/src/libprs500/gui2/images/plus.png
new file mode 100644
index 0000000000..02b399e5e6
Binary files /dev/null and b/src/libprs500/gui2/images/plus.png differ
diff --git a/src/libprs500/gui2/images/reader.png b/src/libprs500/gui2/images/reader.png
new file mode 100644
index 0000000000..5ad31b0cd8
Binary files /dev/null and b/src/libprs500/gui2/images/reader.png differ
diff --git a/src/libprs500/gui2/images/upload.png b/src/libprs500/gui2/images/upload.png
new file mode 100644
index 0000000000..29b95d17cb
Binary files /dev/null and b/src/libprs500/gui2/images/upload.png differ
diff --git a/src/libprs500/gui2/main.py b/src/libprs500/gui2/main.py
new file mode 100644
index 0000000000..a4d40b115d
--- /dev/null
+++ b/src/libprs500/gui2/main.py
@@ -0,0 +1,86 @@
+## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License along
+## with this program; if not, write to the Free Software Foundation, Inc.,
+## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.Warning
+import os, tempfile, sys
+
+from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
+ QSettings, QVariant, QSize, QEventLoop, QString, \
+ QBuffer, QIODevice, QModelIndex
+from PyQt4.QtGui import QPixmap, QErrorMessage, QLineEdit, \
+ QMessageBox, QFileDialog, QIcon, QDialog, QInputDialog
+from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical
+
+from libprs500.gui2 import APP_TITLE, installErrorHandler
+from libprs500.gui2.main_ui import Ui_MainWindow
+
+class Main(QObject, Ui_MainWindow):
+
+ def __init__(self, window):
+ QObject.__init__(self)
+ Ui_MainWindow.__init__(self)
+ self.window = window
+ self.setupUi(window)
+ self.read_settings()
+
+ ####################### Setup books view ########################
+ self.library_view.set_database(self.database_path)
+ self.library_view.connect_to_search_box(self.search)
+
+ window.closeEvent = self.close_event
+ window.show()
+ self.library_view.migrate_database()
+ self.library_view.sortByColumn(3, Qt.DescendingOrder)
+ self.library_view.resizeColumnsToContents()
+ self.library_view.resizeRowsToContents()
+ self.search.setFocus(Qt.OtherFocusReason)
+
+ 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("~/library1.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 main():
+ 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)
+ from PyQt4.Qt import QApplication, QMainWindow
+ app = QApplication(sys.argv)
+ window = QMainWindow()
+ window.setWindowTitle(APP_TITLE)
+ #window.setWindowIcon(QIcon(":/icon"))
+ installErrorHandler(QErrorMessage(window))
+ QCoreApplication.setOrganizationName("KovidsBrain")
+ QCoreApplication.setApplicationName(APP_TITLE)
+ Main(window)
+ return app.exec_()
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/src/libprs500/gui2/main.ui b/src/libprs500/gui2/main.ui
new file mode 100644
index 0000000000..631ee38401
--- /dev/null
+++ b/src/libprs500/gui2/main.ui
@@ -0,0 +1,405 @@
+
+ Kovid Goyal
+ MainWindow
+
+
+
+ 0
+ 0
+ 728
+ 822
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
+
+ 6
+
+
+ 9
+
+
+ 9
+
+
+ 9
+
+
+ 9
+
+ -
+
+
+ 6
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 10000
+ 90
+
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+ QAbstractItemView::DragDrop
+
+
+ QListView::TopToBottom
+
+
+ 20
+
+
+ QListView::IconMode
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 90
+
+
+
+ For help visit <a href="https://libprs500.kovidgoyal.net/wiki/GuiUsage">http://libprs500.kovidgoyal.net</a><br><br><b>libprs500</b>: %1 by <b>Kovid Goyal</b> © 2006<br>%2 %3 %4
+
+
+ Qt::RichText
+
+
+ true
+
+
+
+
+
+ -
+
+
+ 6
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ &Search:
+
+
+ search
+
+
+
+ -
+
+
+ true
+
+
+ false
+
+
+ Search the list of books by title or author<br><br>Words separated by spaces are ANDed
+
+
+ Search the list of books by title, author, publisher, tags and comments<br><br>Words separated by spaces are ANDed
+
+
+ false
+
+
+
+
+
+ false
+
+
+
+ -
+
+
+ Reset Quick Search
+
+
+ ...
+
+
+ :/images/clear.png
+
+
+
+
+
+ -
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 0
+ 10
+
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ QAbstractItemView::DragDrop
+
+
+ true
+
+
+ QAbstractItemView::SelectRows
+
+
+ false
+
+
+
+
+
+ -
+
+
+ 6
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 60
+ 80
+
+
+
+ true
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ <table><tr><td><b>Title: </b>%1</td><td><b> Size:</b> %2</td></tr><tr><td><b>Author: </b>%3</td><td><b> Type: </b>%4</td></tr></table>
+
+
+ Qt::RichText
+
+
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+ Qt::Horizontal
+
+
+
+ 64
+ 64
+
+
+
+ Qt::ToolButtonTextUnderIcon
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+
+ :/images/addfile.png
+
+
+ Add books to Library
+
+
+ A
+
+
+ false
+
+
+
+
+ :/images/delfile.png
+
+
+ Delete books
+
+
+ Del
+
+
+
+
+ :/images/edit.png
+
+
+ Edit meta-information
+
+
+ E
+
+
+ false
+
+
+
+
+
+ CoverDisplay
+ QLabel
+
+
+
+ DeviceView
+ QListView
+
+
+
+ BooksView
+ QTableView
+
+
+
+ SearchBox
+ QLineEdit
+
+
+
+
+
+
+
+
+ clear_button
+ clicked()
+ search
+ clear()
+
+
+ 853
+ 61
+
+
+ 784
+ 58
+
+
+
+
+
diff --git a/src/libprs500/gui2/main_ui.py b/src/libprs500/gui2/main_ui.py
new file mode 100644
index 0000000000..14c98d18d0
--- /dev/null
+++ b/src/libprs500/gui2/main_ui.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'main.ui'
+#
+# Created: Sun May 27 10:53:01 2007
+# by: PyQt4 UI code generator 4-snapshot-20070525
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt4 import QtCore, QtGui
+
+class Ui_MainWindow(object):
+ def setupUi(self, MainWindow):
+ MainWindow.setObjectName("MainWindow")
+ MainWindow.resize(QtCore.QSize(QtCore.QRect(0,0,728,822).size()).expandedTo(MainWindow.minimumSizeHint()))
+
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
+ MainWindow.setSizePolicy(sizePolicy)
+
+ self.centralwidget = QtGui.QWidget(MainWindow)
+ self.centralwidget.setObjectName("centralwidget")
+
+ self.vboxlayout = QtGui.QVBoxLayout(self.centralwidget)
+ self.vboxlayout.setSpacing(6)
+
+
+
+
+
+
+ self.hboxlayout = QtGui.QHBoxLayout()
+ self.hboxlayout.setSpacing(6)
+
+
+
+
+
+
+ self.device_tree = DeviceView(self.centralwidget)
+
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.device_tree.sizePolicy().hasHeightForWidth())
+ self.device_tree.setSizePolicy(sizePolicy)
+ self.device_tree.setMaximumSize(QtCore.QSize(10000,90))
+ self.device_tree.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self.device_tree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self.device_tree.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
+ self.device_tree.setFlow(QtGui.QListView.TopToBottom)
+ self.device_tree.setSpacing(20)
+ self.device_tree.setViewMode(QtGui.QListView.IconMode)
+ self.device_tree.setObjectName("device_tree")
+ self.hboxlayout.addWidget(self.device_tree)
+
+ self.df = QtGui.QLabel(self.centralwidget)
+
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.df.sizePolicy().hasHeightForWidth())
+ self.df.setSizePolicy(sizePolicy)
+ self.df.setMaximumSize(QtCore.QSize(16777215,90))
+ self.df.setTextFormat(QtCore.Qt.RichText)
+ self.df.setOpenExternalLinks(True)
+ self.df.setObjectName("df")
+ self.hboxlayout.addWidget(self.df)
+ self.vboxlayout.addLayout(self.hboxlayout)
+
+ self.hboxlayout1 = QtGui.QHBoxLayout()
+ self.hboxlayout1.setSpacing(6)
+
+
+
+
+ self.hboxlayout1.setObjectName("hboxlayout1")
+
+ self.label = QtGui.QLabel(self.centralwidget)
+ self.label.setObjectName("label")
+ self.hboxlayout1.addWidget(self.label)
+
+ self.search = SearchBox(self.centralwidget)
+ self.search.setEnabled(True)
+ self.search.setAcceptDrops(False)
+ self.search.setAutoFillBackground(False)
+ self.search.setFrame(True)
+ self.search.setObjectName("search")
+ self.hboxlayout1.addWidget(self.search)
+
+ self.clear_button = QtGui.QToolButton(self.centralwidget)
+ self.clear_button.setIcon(QtGui.QIcon(":/images/clear.png"))
+ self.clear_button.setObjectName("clear_button")
+ self.hboxlayout1.addWidget(self.clear_button)
+ self.vboxlayout.addLayout(self.hboxlayout1)
+
+ self.gridlayout = QtGui.QGridLayout()
+
+
+
+
+ self.gridlayout.setHorizontalSpacing(6)
+ self.gridlayout.setVerticalSpacing(6)
+ self.gridlayout.setObjectName("gridlayout")
+
+ self.library_view = BooksView(self.centralwidget)
+
+ sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,QtGui.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(10)
+ sizePolicy.setHeightForWidth(self.library_view.sizePolicy().hasHeightForWidth())
+ self.library_view.setSizePolicy(sizePolicy)
+ self.library_view.setAcceptDrops(True)
+ self.library_view.setDragEnabled(True)
+ self.library_view.setDragDropOverwriteMode(False)
+ self.library_view.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
+ self.library_view.setAlternatingRowColors(True)
+ self.library_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+ self.library_view.setShowGrid(False)
+ self.library_view.setObjectName("library_view")
+ self.gridlayout.addWidget(self.library_view,1,0,1,1)
+ self.vboxlayout.addLayout(self.gridlayout)
+
+ self.hboxlayout2 = QtGui.QHBoxLayout()
+ self.hboxlayout2.setSpacing(6)
+
+
+
+
+ self.hboxlayout2.setObjectName("hboxlayout2")
+
+ self.book_cover = CoverDisplay(self.centralwidget)
+ self.book_cover.setMaximumSize(QtCore.QSize(60,80))
+ self.book_cover.setAcceptDrops(True)
+ self.book_cover.setScaledContents(True)
+ self.book_cover.setObjectName("book_cover")
+ self.hboxlayout2.addWidget(self.book_cover)
+
+ self.book_info = QtGui.QLabel(self.centralwidget)
+ self.book_info.setTextFormat(QtCore.Qt.RichText)
+ self.book_info.setObjectName("book_info")
+ self.hboxlayout2.addWidget(self.book_info)
+ self.vboxlayout.addLayout(self.hboxlayout2)
+ MainWindow.setCentralWidget(self.centralwidget)
+
+ self.tool_bar = QtGui.QToolBar(MainWindow)
+ self.tool_bar.setMinimumSize(QtCore.QSize(0,0))
+ self.tool_bar.setMovable(False)
+ self.tool_bar.setOrientation(QtCore.Qt.Horizontal)
+ self.tool_bar.setIconSize(QtCore.QSize(64,64))
+ self.tool_bar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
+ self.tool_bar.setObjectName("tool_bar")
+ MainWindow.addToolBar(self.tool_bar)
+
+ self.action_add = QtGui.QAction(MainWindow)
+ self.action_add.setIcon(QtGui.QIcon(":/images/addfile.png"))
+ self.action_add.setAutoRepeat(False)
+ self.action_add.setObjectName("action_add")
+
+ self.action_del = QtGui.QAction(MainWindow)
+ self.action_del.setIcon(QtGui.QIcon(":/images/delfile.png"))
+ self.action_del.setObjectName("action_del")
+
+ self.action_edit = QtGui.QAction(MainWindow)
+ self.action_edit.setIcon(QtGui.QIcon(":/images/edit.png"))
+ self.action_edit.setAutoRepeat(False)
+ self.action_edit.setObjectName("action_edit")
+ self.tool_bar.addAction(self.action_add)
+ self.tool_bar.addAction(self.action_del)
+ self.tool_bar.addAction(self.action_edit)
+ self.label.setBuddy(self.search)
+
+ self.retranslateUi(MainWindow)
+ QtCore.QObject.connect(self.clear_button,QtCore.SIGNAL("clicked()"),self.search.clear)
+ QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+ def retranslateUi(self, MainWindow):
+ self.df.setText(QtGui.QApplication.translate("MainWindow", "For help visit http://libprs500.kovidgoyal.net
libprs500: %1 by Kovid Goyal © 2006
%2 %3 %4", None, QtGui.QApplication.UnicodeUTF8))
+ self.label.setText(QtGui.QApplication.translate("MainWindow", "&Search:", None, QtGui.QApplication.UnicodeUTF8))
+ self.search.setToolTip(QtGui.QApplication.translate("MainWindow", "Search the list of books by title or author
Words separated by spaces are ANDed", None, QtGui.QApplication.UnicodeUTF8))
+ self.search.setWhatsThis(QtGui.QApplication.translate("MainWindow", "Search the list of books by title or author
Words separated by spaces are ANDed", None, QtGui.QApplication.UnicodeUTF8))
+ self.clear_button.setToolTip(QtGui.QApplication.translate("MainWindow", "Reset Quick Search", None, QtGui.QApplication.UnicodeUTF8))
+ self.clear_button.setText(QtGui.QApplication.translate("MainWindow", "...", None, QtGui.QApplication.UnicodeUTF8))
+ self.book_info.setText(QtGui.QApplication.translate("MainWindow", "
Title: %1 | Size: %2 |
Author: %3 | Type: %4 |
", None, QtGui.QApplication.UnicodeUTF8))
+ self.action_add.setText(QtGui.QApplication.translate("MainWindow", "Add books to Library", None, QtGui.QApplication.UnicodeUTF8))
+ self.action_add.setShortcut(QtGui.QApplication.translate("MainWindow", "A", None, QtGui.QApplication.UnicodeUTF8))
+ self.action_del.setText(QtGui.QApplication.translate("MainWindow", "Delete books", None, QtGui.QApplication.UnicodeUTF8))
+ self.action_del.setShortcut(QtGui.QApplication.translate("MainWindow", "Del", None, QtGui.QApplication.UnicodeUTF8))
+ self.action_edit.setText(QtGui.QApplication.translate("MainWindow", "Edit meta-information", None, QtGui.QApplication.UnicodeUTF8))
+ self.action_edit.setShortcut(QtGui.QApplication.translate("MainWindow", "E", None, QtGui.QApplication.UnicodeUTF8))
+
+from widgets import DeviceView, CoverDisplay
+from library import BooksView, SearchBox
+import images_rc
diff --git a/src/libprs500/gui2/widgets.py b/src/libprs500/gui2/widgets.py
new file mode 100644
index 0000000000..a23d47eb9c
--- /dev/null
+++ b/src/libprs500/gui2/widgets.py
@@ -0,0 +1,863 @@
+## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License along
+## with this program; if not, write to the Free Software Foundation, Inc.,
+## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+import re
+import os
+import textwrap
+import time
+import traceback
+from operator import itemgetter, attrgetter
+from socket import gethostname
+from urlparse import urlparse, urlunparse
+from urllib import quote, unquote
+from math import sin, cos, pi
+
+from libprs500.gui import Error, _Warning
+from libprs500.ptempfile import PersistentTemporaryFile
+from libprs500 import iswindows
+
+from PyQt4.QtCore import Qt, SIGNAL
+from PyQt4.Qt import QApplication, QString, QFont, QAbstractListModel, \
+ QVariant, QAbstractTableModel, QTableView, QListView, \
+ QLabel, QAbstractItemView, QPixmap, QIcon, QSize, \
+ QSpinBox, QPoint, QPainterPath, QItemDelegate, QPainter, \
+ QPen, QColor, QLinearGradient, QBrush, QStyle, \
+ QByteArray, QBuffer, QMimeData, \
+ 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):
+ """
+ Assumes qba is encoded in ASCII which is usually fine, since
+ this method is used mainly for escaped URIs.
+ @type qba: QByteArray
+ """
+ return str(QString.fromAscii(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 iswindows and path.startswith('/'):
+ path = path[1:]
+ if not os.access(path, os.R_OK):
+ _Warning("You do not have read permission for: " + path, None)
+ 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 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(_file.name.encode('utf-8')), '', '', '')))
+ 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 = PersistentTemporaryFile(suffix="."+ext)
+ files.append(f)
+ return self.drag_object_from_files(files), self._dragged_files
+
+
+class TableView(FileDragAndDrop, QTableView):
+ wrapper = textwrap.TextWrapper(width=20)
+
+ def __init__(self, parent):
+ FileDragAndDrop.__init__(self, QTableView)
+ QTableView.__init__(self, parent)
+
+ @classmethod
+ def wrap(cls, s, width=20):
+ cls.wrapper.width = width
+ return cls.wrapper.fill(s)
+
+ @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 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]
+ _file.close()
+ drag.setPixmap(self.pixmap().scaledToHeight(68, \
+ Qt.SmoothTransformation))
+ self.pixmap().save(os.path.abspath(_file.name))
+ drag.start(Qt.MoveAction)
+
+class DeviceView(FileDragAndDrop, QListView):
+ 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_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)
+
+class DeviceBooksView(TableView):
+ 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 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.
+
+
+ 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 createEditor(self, parent, option, index):
+ if index.column() != 4:
+ return QItemDelegate.createEditor(self, parent, option, index)
+ editor = QSpinBox(parent)
+ editor.setSuffix(" stars")
+ editor.setMinimum(0)
+ editor.setMaximum(5)
+ editor.installEventFilter(self)
+ return editor
+
+ def setEditorData(self, editor, index):
+ if index.column() != 4:
+ return QItemDelegate.setEditorData(self, editor, index)
+ val = index.model()._data[index.row()]["rating"]
+ if not val: val = 0
+ editor.setValue(val)
+
+ def setModelData(self, editor, model, index):
+ if index.column() != 4:
+ return QItemDelegate.setModelData(self, editor, model, index)
+ editor.interpretText()
+ index.model().setData(index, QVariant(editor.value()), Qt.EditRole)
+
+ def updateEditorGeometry(self, editor, option, index):
+ if index.column() != 4:
+ return QItemDelegate.updateEditorGeometry(self, editor, option, index)
+ editor.setGeometry(option.rect)
+
+
+
+class LibraryBooksModel(QAbstractTableModel):
+ FIELDS = ["id", "title", "authors", "size", "date", "rating", "publisher", \
+ "tags", "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 = PersistentTemporaryFile(suffix=name)
+ if not fmt:
+ continue
+ file.write(fmt)
+ file.close()
+ files.append(file)
+ return files
+
+ def update_cover(self, index, pix):
+ spix = pix.scaledToHeight(68, Qt.SmoothTransformation)
+ _id = self.id_from_index(index)
+ qb, sqb = QBuffer(), QBuffer()
+ qb.open(QBuffer.ReadWrite)
+ sqb.open(QBuffer.ReadWrite)
+ pix.save(qb, "JPG")
+ spix.save(sqb, "JPG")
+ data = str(qb.data())
+ sdata = str(sqb.data())
+ qb.close()
+ sqb.close()
+ self.db.update_cover(_id, data, scaled=sdata)
+ self.refresh_row(index.row())
+
+ 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
+ _id = self.id_from_index(index)
+ self.db.add_format(_id, ext, f)
+ f.close()
+ self.refresh_row(index.row())
+ self.emit(SIGNAL('formats_added'), index)
+
+ def rowCount(self, parent):
+ return len(self._data)
+
+ def columnCount(self, parent):
+ return len(self.FIELDS)-3
+
+ def setData(self, index, value, role):
+ done = False
+ if role == Qt.EditRole:
+ row = index.row()
+ _id = self._data[row]["id"]
+ col = index.column()
+ val = unicode(value.toString().toUtf8(), 'utf-8').strip()
+ 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 update_tags_and_comments(self, index, tags, comments):
+ _id = self.id_from_index(index)
+ self.db.set_metadata_item(_id, "tags", tags)
+ self.db.set_metadata_item(_id, "comments", comments)
+ self.refresh_row(index.row())
+
+ 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 = ""
+ comments = TableView.wrap(comments, width=80)
+ 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):
+ datum = self.db.get_row_by_id(self._data[row]["id"], self.FIELDS)
+ self._data[row:row+1] = [datum]
+ for i in range(len(self._orig_data)):
+ if self._orig_data[i]["id"] == datum["id"]:
+ self._orig_data[i:i+1] = [datum]
+ 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=35)
+ elif col == 1:
+ au = row["authors"]
+ if au:
+ au = au.split("&")
+ jau = [ TableView.wrap(a, width=30).strip() for a in au ]
+ text = "\n".join(jau)
+ 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)
+ elif role == Qt.ToolTipRole and index.isValid():
+ if index.column() in [0, 1, 4, 5]:
+ edit = "Double click to edit me
"
+ else:
+ edit = ""
+ return QVariant(edit + "You can drag and drop me to the \
+ desktop to save all my formats to your hard disk.")
+ 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))
+
+class DeviceBooksModel(QAbstractTableModel):
+ @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 set_data(self, book_list):
+ self._data = book_list
+ self._orig_data = book_list
+ self.reset()
+
+ def rowCount(self, parent):
+ return len(self._data)
+
+ def columnCount(self, parent):
+ return 4
+
+ def headerData(self, section, orientation, role):
+ if role != Qt.DisplayRole:
+ return NONE
+ text = ""
+ if orientation == Qt.Horizontal:
+ if section == 0: text = "Title"
+ elif section == 1: text = "Author(s)"
+ elif section == 2: text = "Size"
+ elif section == 3: text = "Date"
+ return QVariant(self.trUtf8(text))
+ else: return QVariant(str(1+section))
+
+ def data(self, index, role):
+ if role == Qt.DisplayRole:
+ row, col = index.row(), index.column()
+ book = self._data[row]
+ if col == 0:
+ text = TableView.wrap(book.title, width=40)
+ elif col == 1:
+ au = book.author
+ au = au.split("&")
+ jau = [ TableView.wrap(a, width=25).strip() for a in au ]
+ text = "\n".join(jau)
+ 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
+
+
+
+
+
+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("layoutChanged()"))
+
+ def rowCount(self, parent):
+ base = 1
+ if self.show_reader:
+ base += 1
+ if self.show_card:
+ base += 1
+ return base
+
+ 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