Basic GUI2 job control
This commit is contained in:
Kovid Goyal 2007-06-22 17:34:11 +00:00
parent 6c63fc0ec7
commit 8989b541f1
16 changed files with 424 additions and 970 deletions

View File

@ -13,7 +13,7 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
''' E-book management software'''
__version__ = "0.3.54"
__version__ = "0.3.55"
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
__appname__ = 'libprs500'

View File

@ -31,7 +31,7 @@ class Device(object):
# Ordered list of supported formats
FORMATS = ["lrf", "rtf", "pdf", "txt"]
VENDOR_ID = 0x0000
PRODICT_ID = 0x0000
PRODUCT_ID = 0x0000
def __init__(self, key='-1', log_packets=False, report_progress=None) :
"""
@ -49,6 +49,15 @@ class Device(object):
'''Return True iff the device is physically connected to the computer'''
raise NotImplementedError()
def set_progress_reporter(self, report_progress):
'''
@param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks
If it is called with -1 that means that the
task does not have any progress information
'''
raise NotImplementedError()
def get_device_information(self, end_session=True):
"""
Ask device for device information. See L{DeviceInfoQuery}.
@ -56,6 +65,12 @@ class Device(object):
"""
raise NotImplementedError()
def card_prefix(self, end_session=True):
'''
Return prefix to paths on the card or None if no cards present.
'''
raise NotImplementedError()
def total_space(self, end_session=True):
"""
Get total space available on the mountpoints:
@ -72,11 +87,11 @@ class Device(object):
"""
Get free space available on the mountpoints:
1. Main memory
2. Memory Stick
3. SD Card
2. Card A
3. Card B
@return: A 3 element list with free space in bytes of (1, 2, 3). If a
particular device doesn't have any of these locations it should return 0.
particular device doesn't have any of these locations it should return -1.
"""
raise NotImplementedError()
@ -84,8 +99,8 @@ class Device(object):
"""
Return a list of ebooks on the device.
@param oncard: If True return a list of ebooks on the storage card,
otherwise return list of ebooks in main memory of device
otherwise return list of ebooks in main memory of device.
If True and no books on card return empty list.
@return: A list of Books. Each Book object must have the fields:
title, author, size, datetime (a time tuple), path, thumbnail (can be None).
"""

View File

@ -224,6 +224,8 @@ class PRS500(Device):
"""
return get_device_by_id(cls.VENDOR_ID, cls.PRODUCT_ID) != None
def set_progress_reporter(self, report_progress):
self.report_progress = report_progress
def open(self) :
"""
@ -585,6 +587,21 @@ class PRS500(Device):
data.append( pkt.total )
return data
@safe
def card_prefix(self, end_session=True):
'''Return prefix of path to card or None if no cards present'''
try:
path = 'a:/'
self.path_properties(path, end_session=False)
return path
except PathError:
try:
path = 'b:/'
self.path_properties(path, end_session=False)
return path
except PathError:
return None
@safe
def free_space(self, end_session=True):
"""

View File

@ -91,7 +91,7 @@ def option_parser(usage):
''' Supported profiles: '''+', '.join(profiles))
page.add_option('--left-margin', default=20, dest='left_margin', type='int',
help='''Left margin of page. Default is %default px.''')
page.add_option('--right-margin', default=5, dest='right_margin', type='int',
page.add_option('--right-margin', default=20, dest='right_margin', type='int',
help='''Right margin of page. Default is %default px.''')
page.add_option('--top-margin', default=10, dest='top_margin', type='int',
help='''Top margin of page. Default is %default px.''')

View File

@ -106,26 +106,26 @@ class Span(_Span):
def font_size(val):
ans = None
unit = Span.unit_convert(val, dpi, 14)
unit = Span.unit_convert(val, dpi, 14)
if unit:
# Assume a 10 pt font (14 pixels) has fontsize 100
ans = int(unit * (72./dpi) * 10)
else:
if "xx-small" in val:
ans = 40
elif "x-small" in val >= 0:
elif "x-small" in val:
ans = 60
elif "small" in val:
ans = 80
elif "xx-large" in val:
ans = 180
elif "x-large" in val >= 0:
elif "x-large" in val:
ans = 140
elif "large" in val >= 0:
elif "large" in val:
ans = 120
if ans is not None:
ans += int(font_delta * 20)
ans = str(ans)
ans = str(ans)
return ans
t = dict()
@ -262,20 +262,21 @@ class HTMLConverter(object):
'''
# Defaults for various formatting tags
self.css = dict(
h1 = {"font-size" :"xx-large", "font-weight":"bold", 'text-indent':'0pt'},
h2 = {"font-size" :"x-large", "font-weight":"bold", 'text-indent':'0pt'},
h3 = {"font-size" :"large", "font-weight":"bold", 'text-indent':'0pt'},
h4 = {"font-size" :"large", 'text-indent':'0pt'},
h5 = {"font-weight" :"bold", 'text-indent':'0pt'},
b = {"font-weight" :"bold"},
strong = {"font-weight" :"bold"},
i = {"font-style" :"italic"},
em = {"font-style" :"italic"},
small = {'font-size' :'small'},
pre = {'font-family' :'monospace' },
tt = {'font-family' :'monospace'},
h1 = {"font-size" : "xx-large", "font-weight":"bold", 'text-indent':'0pt'},
h2 = {"font-size" : "x-large", "font-weight":"bold", 'text-indent':'0pt'},
h3 = {"font-size" : "large", "font-weight":"bold", 'text-indent':'0pt'},
h4 = {"font-size" : "large", 'text-indent':'0pt'},
h5 = {"font-weight" : "bold", 'text-indent':'0pt'},
b = {"font-weight" : "bold"},
strong = {"font-weight" : "bold"},
i = {"font-style" : "italic"},
em = {"font-style" : "italic"},
small = {'font-size' : 'small'},
pre = {'font-family' : 'monospace' },
tt = {'font-family' : 'monospace'},
center = {'text-align' : 'center'},
th = {'font-size':'large', 'font-weight':'bold'},
th = {'font-size' : 'large', 'font-weight':'bold'},
big = {'font-size' : 'large', 'font-weight':'bold'},
)
self.profile = profile #: Defines the geometry of the display device
self.chapter_detection = chapter_detection #: Flag to toggle chapter detection
@ -389,6 +390,8 @@ class HTMLConverter(object):
prop.update(temp)
prop = dict()
if parent_css:
merge_parent_css(prop, parent_css)
if tag.has_key("align"):
prop["text-align"] = tag["align"]
if self.css.has_key(tag.name):
@ -398,8 +401,6 @@ class HTMLConverter(object):
for classname in ["."+cls, tag.name+"."+cls]:
if self.css.has_key(classname):
prop.update(self.css[classname])
if parent_css:
merge_parent_css(prop, parent_css)
if tag.has_key("style"):
prop.update(self.parse_style_properties(tag["style"]))
return prop
@ -1043,7 +1044,7 @@ class HTMLConverter(object):
self.end_current_para()
if tagname.startswith('h'):
self.current_block.append(CR())
elif tagname in ['b', 'strong', 'i', 'em', 'span', 'tt']:
elif tagname in ['b', 'strong', 'i', 'em', 'span', 'tt', 'big']:
self.process_children(tag, tag_css)
elif tagname == 'font':
if tag.has_key('face'):

View File

@ -0,0 +1,15 @@
## 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.

View File

@ -0,0 +1,96 @@
## 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.
import os, sys, shutil, glob
from tempfile import mkdtemp
from subprocess import Popen, PIPE
from libprs500.ebooks.lrf import option_parser, ConversionError
from libprs500.ebooks.lrf.html.convert_from import parse_options as html_parse_options
from libprs500.ebooks.lrf.html.convert_from import process_file
from libprs500 import isosx
CLIT = 'clit'
if isosx and hasattr(sys, 'frameworks_dir'):
CLIT = os.path.join(sys.frameworks_dir, CLIT)
def parse_options(cli=True):
""" CLI for lit -> lrf conversions """
parser = option_parser(
"""usage: %prog [options] mybook.lit
%prog converts mybook.lit to mybook.lrf
"""
)
options, args = parser.parse_args()
if len(args) != 1:
if cli:
parser.print_help()
raise ConversionError, 'no filename specified'
return options, args, parser
def generate_html(pathtolit):
if not os.access(pathtolit, os.R_OK):
raise ConversionError, 'Cannot read from ' + pathtolit
tdir = mkdtemp(prefix='libprs500_lit2lrf_')
cmd = ' '.join([CLIT, '"'+pathtolit+'"', tdir])
p = Popen(cmd, shell=True, stderr=PIPE)
ret = p.wait()
if ret != 0:
shutil.rmtree(tdir)
err = p.stderr.read()
raise ConversionError, err
return tdir
def main():
try:
options, args, parser = parse_options()
lit = os.path.abspath(os.path.expanduser(args[0]))
tdir = generate_html(lit)
try:
l = glob.glob(os.path.join(tdir, '*toc*.htm*'))
if not l:
l = glob.glob(os.path.join(tdir, '*top*.htm*'))
if not l:
raise ConversionError, 'Conversion of lit to html failed.'
htmlfile = l[0]
for i in range(1, len(sys.argv)):
if sys.argv[i] == args[0]:
sys.argv.remove(sys.argv[i])
break
sys.argv.append(htmlfile)
o_spec = False
for arg in sys.argv[1:]:
arg = arg.lstrip()
if arg.startswith('-o') or arg.startswith('--output'):
o_spec = True
break
ext = '.lrf'
for arg in sys.argv[1:]:
if arg.strip() == '--lrs':
ext = '.lrs'
break
if not o_spec:
sys.argv.append('-o')
sys.argv.append(os.path.splitext(os.path.basename(lit))[0]+ext)
options, args, parser = html_parse_options(parser=parser)
process_file(htmlfile, options)
finally:
shutil.rmtree(tdir)
except ConversionError, err:
print >>sys.stderr, err
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -15,13 +15,10 @@
""" The GUI for libprs500. """
import sys, os, re, StringIO, traceback
from PyQt4.QtCore import QVariant
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
APP_TITLE = "libprs500"
from libprs500 import __appname__ as APP_TITLE
from libprs500 import __author__
NONE = QVariant() #: Null value to return from the data function of item models
error_dialog = None
def extension(path):
@ -49,4 +46,19 @@ def Error(msg, e):
msg = re.sub(r"\n", "<br>", msg)
error_dialog.showMessage(msg)
error_dialog.show()
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

View File

@ -12,11 +12,18 @@
## 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
from PyQt4.QtCore import QThread, SIGNAL
import traceback
from PyQt4.QtCore import QThread, SIGNAL, QObject, Qt
from libprs500.devices.prs500.driver import PRS500
class DeviceDetector(QThread):
'''
Worker thread that polls the USB ports for devices. Emits the
signal connected(PyQt_PyObject, PyQt_PyObject) on connection and
disconnection events.
'''
def __init__(self, sleep_time=2000):
'''
@param sleep_time: Time to sleep between device probes in millisecs
@ -36,4 +43,65 @@ class DeviceDetector(QThread):
elif not connected and device[1]:
self.emit(SIGNAL('connected(PyQt_PyObject, PyQt_PyObject)'), device[0], False)
device[1] ^= True
self.msleep(self.sleep_time)
self.msleep(self.sleep_time)
class DeviceJob(QThread):
'''
Worker thread that communicates with device.
'''
def __init__(self, id, mutex, func, *args, **kwargs):
QThread.__init__(self)
self.id = id
self.func = func
self.args = args
self.kwargs = kwargs
self.mutex = mutex
self.result = None
def run(self):
if self.mutex != None:
self.mutex.lock()
last_traceback, exception = None, None
try:
try:
self.result = self.func(self.progress_update, *self.args, **self.kwargs)
except Exception, err:
exception = err
last_traceback = traceback.format_exc()
finally:
if self.mutex != None:
self.mutex.unlock()
self.emit(SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.id, self.result, exception, last_traceback)
def progress_update(self, val):
print val
self.emit(SIGNAL('status_update(int)'), int(val), Qt.QueuedConnection)
class DeviceManager(QObject):
def __init__(self, device_class):
QObject.__init__(self)
self.device_class = device_class
self.device = device_class()
def get_info_func(self):
''' Return callable that returns device information and free space on device'''
def get_device_information(updater):
self.device.set_updater(updater)
info = self.device.get_device_information(end_session=False)
info = {'name':info[0], 'version':info[1], 'swversion':[2], 'mimetype':info[3]}
cp = self.device.card_prefix(end_session=False)
fs = self.device.free_space()
fs = {'main':fs[0], 'carda':fs[1], 'cardb':fs[2]}
return info, cp, fs
return get_device_information
def books_func(self):
'''Return callable that returns the list of books on device as two booklists'''
def books(updater):
self.device.set_updater(updater)
mainlist = self.device.books(oncard=False, end_session=False)
cardlist = self.device.books(oncard=True)
return mainlist, cardlist
return books

Binary file not shown.

View File

@ -0,0 +1,84 @@
## 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.
from PyQt4.QtCore import QAbstractTableModel, QMutex, QObject, SIGNAL
from libprs500.gui2.device import DeviceJob
class JobException(Exception):
pass
class JobManager(QAbstractTableModel):
def __init__(self):
QAbstractTableModel.__init__(self)
self.jobs = {}
self.next_id = 0
self.job_create_lock = QMutex()
self.job_remove_lock = QMutex()
self.device_lock = QMutex()
self.cleanup_lock = QMutex()
self.cleanup = {}
def create_job(self, job_class, lock, *args, **kwargs):
self.job_create_lock.lock()
try:
self.next_id += 1
job = job_class(self.next_id, lock, *args, **kwargs)
QObject.connect(job, SIGNAL('finished()'), self.cleanup_jobs)
self.jobs[self.next_id] = job
return job
finally:
self.job_create_lock.unlock()
def run_device_job(self, slot, callable, *args, **kwargs):
'''
Run a job to communicate with the device.
@param slot: The function to call with the job result. It is called with
the parameters id, result, exception, formatted_traceback
@param callable: The function to call to communicate with the device.
@param args: The arguments to pass to callable
@param kwargs: The keyword arguments to pass to callable
'''
job = self.create_job(DeviceJob, self.device_lock, callable, *args, **kwargs)
QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.job_done)
QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
slot)
job.start()
def job_done(self, id, *args, **kwargs):
'''
Slot that is called when a job is completed.
'''
self.job_remove_lock.lock()
try:
job = self.jobs.pop(id)
self.cleanup_lock.lock()
self.cleanup[id] = job
self.cleanup_lock.unlock()
finally:
self.job_remove_lock.unlock()
def cleanup_jobs(self):
self.cleanup_lock.lock()
toast = []
for id in self.cleanup.keys():
if not self.cleanup[id].isRunning():
toast.append(id)
for id in toast:
self.cleanup.pop(id)
self.cleanup_lock.unlock()

View File

@ -21,10 +21,12 @@ from PyQt4.QtGui import QPixmap, QErrorMessage, QLineEdit, \
QMessageBox, QFileDialog, QIcon, QDialog, QInputDialog
from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical
from libprs500 import __version__ as VERSION
from libprs500.gui2 import APP_TITLE, installErrorHandler
from libprs500.gui2.main_ui import Ui_MainWindow
from libprs500.gui2.device import DeviceDetector
from libprs500.gui2.device import DeviceDetector, DeviceManager
from libprs500.gui2.status import StatusBar
from libprs500.gui2.jobs import JobManager, JobException
class Main(QObject, Ui_MainWindow):
@ -34,6 +36,10 @@ class Main(QObject, Ui_MainWindow):
self.window = window
self.setupUi(window)
self.read_settings()
self.job_manager = JobManager()
self.device_manager = None
self.temporary_slots = {}
self.df.setText(self.df.text().arg(VERSION))
####################### Status Bar #####################
self.status_bar = StatusBar()
@ -54,13 +60,23 @@ class Main(QObject, Ui_MainWindow):
####################### Setup device detection ########################
self.detector = DeviceDetector(sleep_time=2000)
QObject.connect(self.detector, SIGNAL('connected(PyQt_PyObject, PyQt_PyObject)'),
self.device_connected, Qt.QueuedConnection)
self.device_detected, Qt.QueuedConnection)
self.detector.start(QThread.InheritPriority)
def device_connected(self, cls, connected):
print cls, connected
def device_detected(self, cls, connected):
if connected:
def info_read(id, result, exception, formatted_traceback):
if exception:
pass #TODO: Handle error
info, cp, fs = result
print self, id, result, exception, formatted_traceback
self.temporary_slots['device_info_read'] = info_read
self.device_manager = DeviceManager(cls)
func = self.device_manager.get_info_func()
self.job_manager.run_device_job(info_read, func)
def read_settings(self):
settings = QSettings()
@ -95,8 +111,16 @@ def main():
installErrorHandler(QErrorMessage(window))
QCoreApplication.setOrganizationName("KovidsBrain")
QCoreApplication.setApplicationName(APP_TITLE)
Main(window)
main = Main(window)
def unhandled_exception(type, value, tb):
import traceback
traceback.print_exception(type, value, tb, file=sys.stderr)
if type == KeyboardInterrupt:
QCoreApplication.exit(1)
sys.excepthook = unhandled_exception
return app.exec_()
if __name__ == '__main__':
main()

View File

@ -42,7 +42,7 @@
<number>0</number>
</property>
<item>
<widget class="DeviceView" name="device_tree" >
<widget class="LocationView" name="device_tree" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
<horstretch>0</horstretch>
@ -90,7 +90,7 @@
</size>
</property>
<property name="text" >
<string>For help visit &lt;a href="https://libprs500.kovidgoyal.net/wiki/GuiUsage">http://libprs500.kovidgoyal.net&lt;/a>&lt;br>&lt;br>&lt;b>libprs500&lt;/b>: %1 by &lt;b>Kovid Goyal&lt;/b> &amp;copy; 2006&lt;br>%2 %3 %4</string>
<string>For help visit &lt;a href="https://libprs500.kovidgoyal.net/wiki/GuiUsage">http://libprs500.kovidgoyal.net&lt;/a>&lt;br>&lt;br>&lt;b>libprs500&lt;/b>: %1 by &lt;b>Kovid Goyal&lt;/b> &amp;copy; 2007&lt;br>%2 %3 %4</string>
</property>
<property name="textFormat" >
<enum>Qt::RichText</enum>
@ -332,11 +332,6 @@
</action>
</widget>
<customwidgets>
<customwidget>
<class>DeviceView</class>
<extends>QListView</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>BooksView</class>
<extends>QTableView</extends>
@ -347,6 +342,11 @@
<extends>QLineEdit</extends>
<header>library.h</header>
</customwidget>
<customwidget>
<class>LocationView</class>
<extends>QListView</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="images.qrc" />

View File

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'main.ui'
#
# Created: Tue Jun 19 11:07:30 2007
# Created: Thu Jun 21 20:31:43 2007
# by: PyQt4 UI code generator 4-snapshot-20070606
#
# WARNING! All changes made in this file will be lost!
@ -32,7 +32,7 @@ class Ui_MainWindow(object):
self.hboxlayout.setMargin(0)
self.hboxlayout.setObjectName("hboxlayout")
self.device_tree = DeviceView(self.centralwidget)
self.device_tree = LocationView(self.centralwidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,QtGui.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
@ -184,7 +184,7 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "libprs500", None, QtGui.QApplication.UnicodeUTF8))
self.df.setText(QtGui.QApplication.translate("MainWindow", "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> &copy; 2006<br>%2 %3 %4", None, QtGui.QApplication.UnicodeUTF8))
self.df.setText(QtGui.QApplication.translate("MainWindow", "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> &copy; 2007<br>%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<br><br>Words separated by spaces are ANDed", None, QtGui.QApplication.UnicodeUTF8))
self.search.setWhatsThis(QtGui.QApplication.translate("MainWindow", "Search the list of books by title, author, publisher, tags and comments<br><br>Words separated by spaces are ANDed", None, QtGui.QApplication.UnicodeUTF8))
@ -197,6 +197,6 @@ class Ui_MainWindow(object):
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
from widgets import LocationView
from library import BooksView, SearchBox
import images_rc

View File

@ -1,87 +0,0 @@
## 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.
from PyQt4.QtGui import QIconEngine, QTabWidget, QPixmap, QIcon, QPainter, QColor
from PyQt4.QtCore import QTimeLine, QObject, SIGNAL
from PyQt4.QtSvg import QSvgRenderer
class RotatingIconEngine(QIconEngine):
@staticmethod
def create_pixmaps(path, size=16, delta=20):
r = QSvgRenderer(path)
if not r.isValid():
raise Exception(path + ' not valid svg')
pixmaps = []
for angle in range(0, 360, delta):
pm = QPixmap(size, size)
pm.fill(QColor(0,0,0,0))
p = QPainter(pm)
p.translate(size/2., size/2.)
p.rotate(angle)
p.translate(-size/2., -size/2.)
r.render(p)
p.end()
pixmaps.append(pm)
return pixmaps
def __init__(self, path, size=16):
self.pixmaps = self.__class__.create_pixmaps(path, size)
self.current = 0
QIconEngine.__init__(self)
def next(self):
self.current += 1
self.current %= len(self.pixmaps)
def reset(self):
self.current = 0
def pixmap(self, size, mode, state):
return self.pixmaps[self.current]
class AnimatedTabWidget(QTabWidget):
def __init__(self, parent):
self.animated_tab = 1
self.ri = RotatingIconEngine(':/images/jobs.svg')
QTabWidget.__init__(self, parent)
self.timeline = QTimeLine(4000, self)
self.timeline.setLoopCount(0)
self.timeline.setCurveShape(QTimeLine.LinearCurve)
self.timeline.setFrameRange(0, len(self.ri.pixmaps))
QObject.connect(self.timeline, SIGNAL('frameChanged(int)'), self.next)
def setup(self):
self.setTabIcon(self.animated_tab, QIcon(self.ri))
def animate(self):
self.timeline.start()
def update_animated_tab(self):
tb = self.tabBar()
rect = tb.tabRect(self.animated_tab)
tb.update(rect)
def stop(self):
self.timeline.stop()
self.ri.reset()
self.update_animated_tab()
def next(self, frame):
self.ri.next()
self.update_animated_tab()

View File

@ -1,4 +1,4 @@
## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net
## 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
@ -12,829 +12,37 @@
## 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
'''
Miscellanous widgets used in the GUI
'''
from PyQt4.QtGui import QListView, QIcon, QFont
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, QSize, SIGNAL
from libprs500.gui import Error, _Warning
from libprs500.ptempfile import PersistentTemporaryFile
from libprs500 import iswindows
from libprs500.gui2 import human_readable, NONE
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)
class LocationModel(QAbstractListModel):
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
QAbstractListModel.__init__(self, parent)
self.icons = [QVariant(QIcon(':/library')),
QVariant(QIcon(':/reader')),
QVariant(QIcon(':/card'))]
self.text = ['Library',
'Reader\n%s available',
'Card\n%s available']
self.free = [-1, -1]
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 <b>edit</b> me<br><br>"
else:
edit = ""
return QVariant(edit + "You can <b>drag and drop</b> 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))
return 1 + sum([1 for i in self.free if i >= 0])
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)
text = self.text[row]%(human_readable(self.free[row-1])) if row > 0 \
else self.text[row]
data = QVariant(text)
elif role == Qt.DecorationRole:
data = self.icons[row]
elif role == Qt.SizeHintRole:
if row == 1:
return QVariant(QSize(150, 70))
@ -844,20 +52,21 @@ class DeviceModel(QAbstractListModel):
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 headerData(self, section, orientation, role):
return NONE
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
def update_devices(self, cp, fs):
self.free[0] = fs[0]
self.free[1] = max(fs[1:])
if cp == None:
self.free[1] = -1
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"),
self.index(1), self.index(2))
class LocationView(QListView):
def __init__(self, parent):
QListView.__init__(self, parent)
self.setModel(LocationModel(self))
self.reset()