2007-01-03 19:22:59 +00:00

649 lines
28 KiB
Python

## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.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.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
DEFAULT_BOOK_COVER = None
LIBRARY_BOOK_TEMPLATE = QString("<table><tr><td><b>Formats:</b> %1 \
</td><td><b>Tags:</b> %2</td></tr> \
<tr><td colspan='2'><b>Comments:</b> %3</td>\
</tr></table>")
DEVICE_BOOK_TEMPLATE = QString("<table><tr><td><b>Title: </b>%1</td><td> \
<b>&nbsp;Size:</b> %2</td></tr>\
<tr><td><b>Author: </b>%3</td>\
<td><b>&nbsp;Type: </b>%4</td></tr></table>")
Ui_MainWindow = import_ui("main.ui")
class Main(QObject, Ui_MainWindow):
def report_error(func):
"""
Decorator to ensure that unhandled exceptions are displayed
to users via the GUI
"""
def function(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception, e:
Error("There was an error calling " + func.__name__, e)
raise
return function
""" 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.action_add.setEnabled(False)
self.action_edit.setEnabled(False)
self.device_view.show()
self.library_view.hide()
self.book_cover.setAcceptDrops(False)
self.device_view.resizeColumnsToContents()
self.device_view.resizeRowsToContents()
else:
self.action_add.setEnabled(True)
self.action_edit.setEnabled(True)
self.device_view.hide()
self.library_view.show()
self.book_cover.setAcceptDrops(True)
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.current_view.selectionModel():
self.current_view.selectionModel().reset()
self.current_view.resizeColumnsToContents()
self.current_view.resizeRowsToContents()
self.book_cover.hide()
self.book_info.hide()
QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
def resize_rows_and_columns(self, topleft, bottomright):
for c in range(topleft.column(), bottomright.column()+1):
self.current_view.resizeColumnToContents(c)
for r in range(topleft.row(), bottomright.row()+1):
self.current_view.resizeRowToContents(c)
def show_book(self, current, previous):
if not len(self.current_view.selectedIndexes()):
return
if self.library_view.isVisible():
formats, tags, comments, cover = self.library_model\
.info(current.row())
data = LIBRARY_BOOK_TEMPLATE.arg(formats).arg(tags).arg(comments)
tooltip = "To save the cover, drag it to the desktop.<br>To \
change the cover drag the new cover onto this picture"
else:
title, author, size, mime, cover = self.device_view.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()
self.current_view.scrollTo(current)
def formats_added(self, index):
if index == self.library_view.currentIndex():
self.show_book(index, index)
@report_error
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 \
<b>permanently delete</b> 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)
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 = unicode(files[0].toUtf8(), 'utf-8')
settings.setValue("add books dialog dir", \
QVariant(os.path.dirname(x)))
files = unicode(files.join("|||").toUtf8(), 'utf-8').split("|||")
self.add_books(files)
@report_error
def add_books(self, files):
self.window.setCursor(Qt.WaitCursor)
try:
for _file in files:
_file = os.path.abspath(_file)
self.library_view.model().add_book(_file)
if self.library_view.isVisible():
if len(str(self.search.text())):
self.search.clear()
else:
self.library_model.search("")
else:
self.library_model.search("")
hv = self.library_view.horizontalHeader()
col = hv.sortIndicatorSection()
order = hv.sortIndicatorOrder()
self.library_view.model().sort(col, order)
finally:
self.window.setCursor(Qt.ArrowCursor)
@report_error
def edit(self, action):
if self.library_view.isVisible():
rows = self.library_view.selectionModel().selectedRows()
accepted = False
for row in rows:
_id = self.library_model.id_from_index(row)
dialog = QDialog(self.window)
ebd = EditBookDialog(dialog, _id, self.library_model.db)
if dialog.exec_() == QDialog.Accepted:
accepted = True
title = unicode(ebd.title.text().toUtf8(), 'utf-8').strip()
authors = unicode(ebd.authors.text().toUtf8(), 'utf-8').strip()
rating = ebd.rating.value()
tags = unicode(ebd.tags.text().toUtf8(), 'utf-8').strip()
publisher = unicode(ebd.publisher.text().toUtf8(), \
'utf-8').strip()
comments = unicode(ebd.comments.toPlainText().toUtf8(), \
'utf-8').strip()
pix = ebd.cover.pixmap()
if not pix.isNull():
self.update_cover(pix)
model = self.library_view.model()
if title:
index = model.index(row.row(), 0)
model.setData(index, QVariant(title), Qt.EditRole)
if authors:
index = model.index(row.row(), 1)
model.setData(index, QVariant(authors), Qt.EditRole)
if publisher:
index = model.index(row.row(), 5)
model.setData(index, QVariant(publisher), Qt.EditRole)
index = model.index(row.row(), 4)
model.setData(index, QVariant(rating), Qt.EditRole)
self.update_tags_and_comments(row, tags, comments)
self.library_model.refresh_row(row.row())
self.show_book(self.current_view.currentIndex(), QModelIndex())
def update_tags_and_comments(self, index, tags, comments):
self.library_model.update_tags_and_comments(index, tags, comments)
@report_error
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)
@report_error
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:
if len(str(self.search.text())):
self.search.clear()
else:
self.device_view.model().search("")
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("libprs500_\S+_......_" + \
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()
@apply
def current_view():
doc = """ The currently visible view """
def fget(self):
return self.library_view if self.library_view.isVisible() \
else self.device_view
return property(doc=doc, fget=fget)
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)
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_rows_and_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.sortByColumn(3, Qt.DescendingOrder)
# 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_rows_and_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()
self.library_view.resizeColumnsToContents()
self.library_view.resizeRowsToContents()
def device_removed(self):
self.df.setText(self.df_template.arg("").arg("").arg(""))
self.device_tree.hide_reader(True)
self.device_tree.hide_card(True)
self.device_tree.selectionModel().reset()
if self.device_view.isVisible():
self.device_view.hide()
self.library_view.selectionModel().reset()
self.library_view.show()
self.book_cover.hide()
self.book_info.hide()
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, err:
qFatal(str(err))
except DeviceError, err:
self.dev.reconnect()
self.thread().msleep(100)
return self.establish_connection()
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 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)
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.", \
dest="log_packets", action="store_true", default=False)
options, args = parser.parse_args()
from PyQt4.Qt import QApplication, QMainWindow
app = QApplication(sys.argv)
global DEFAULT_BOOK_COVER
DEFAULT_BOOK_COVER = QPixmap(":/default_cover")
window = QMainWindow()
window.setWindowTitle(APP_TITLE)
window.setWindowIcon(QIcon(":/icon"))
installErrorHandler(QErrorMessage(window))
QCoreApplication.setOrganizationName("KovidsBrain")
QCoreApplication.setApplicationName(APP_TITLE)
Main(window, options.log_packets)
lock = LockFile(lock)
return app.exec_()
if __name__ == "__main__":
sys.exit(main())