diff --git a/src/libprs500/__init__.py b/src/libprs500/__init__.py index 53f4ddcd7f..ce7df1116e 100644 --- a/src/libprs500/__init__.py +++ b/src/libprs500/__init__.py @@ -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 " __appname__ = 'libprs500' diff --git a/src/libprs500/devices/interface.py b/src/libprs500/devices/interface.py index c20ea429b0..da825c9ed0 100644 --- a/src/libprs500/devices/interface.py +++ b/src/libprs500/devices/interface.py @@ -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). """ diff --git a/src/libprs500/devices/prs500/driver.py b/src/libprs500/devices/prs500/driver.py index 9b86fb3a95..3d9ae5b900 100755 --- a/src/libprs500/devices/prs500/driver.py +++ b/src/libprs500/devices/prs500/driver.py @@ -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): """ diff --git a/src/libprs500/ebooks/lrf/__init__.py b/src/libprs500/ebooks/lrf/__init__.py index 0dd5d78b07..96729b81aa 100644 --- a/src/libprs500/ebooks/lrf/__init__.py +++ b/src/libprs500/ebooks/lrf/__init__.py @@ -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.''') diff --git a/src/libprs500/ebooks/lrf/html/convert_from.py b/src/libprs500/ebooks/lrf/html/convert_from.py index 714d7e05d1..7c05f552f0 100644 --- a/src/libprs500/ebooks/lrf/html/convert_from.py +++ b/src/libprs500/ebooks/lrf/html/convert_from.py @@ -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'): diff --git a/src/libprs500/ebooks/lrf/lit/__init__.py b/src/libprs500/ebooks/lrf/lit/__init__.py new file mode 100644 index 0000000000..97ad144cc4 --- /dev/null +++ b/src/libprs500/ebooks/lrf/lit/__init__.py @@ -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. + diff --git a/src/libprs500/ebooks/lrf/lit/convert_from.py b/src/libprs500/ebooks/lrf/lit/convert_from.py new file mode 100644 index 0000000000..4c6d4608cc --- /dev/null +++ b/src/libprs500/ebooks/lrf/lit/convert_from.py @@ -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() + + \ No newline at end of file diff --git a/src/libprs500/gui2/__init__.py b/src/libprs500/gui2/__init__.py index c7a8556f79..52c20e35ed 100644 --- a/src/libprs500/gui2/__init__.py +++ b/src/libprs500/gui2/__init__.py @@ -15,13 +15,10 @@ """ The GUI for libprs500. """ import sys, os, re, StringIO, traceback from PyQt4.QtCore import QVariant - -__docformat__ = "epytext" -__author__ = "Kovid Goyal " -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", "
", 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 diff --git a/src/libprs500/gui2/device.py b/src/libprs500/gui2/device.py index c4e62445f0..7e5decc019 100644 --- a/src/libprs500/gui2/device.py +++ b/src/libprs500/gui2/device.py @@ -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) \ No newline at end of file + 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 \ No newline at end of file diff --git a/src/libprs500/gui2/images/jobs-animated.mng b/src/libprs500/gui2/images/jobs-animated.mng new file mode 100644 index 0000000000..affe3b69fd Binary files /dev/null and b/src/libprs500/gui2/images/jobs-animated.mng differ diff --git a/src/libprs500/gui2/jobs.py b/src/libprs500/gui2/jobs.py new file mode 100644 index 0000000000..96b05e3433 --- /dev/null +++ b/src/libprs500/gui2/jobs.py @@ -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() + \ No newline at end of file diff --git a/src/libprs500/gui2/main.py b/src/libprs500/gui2/main.py index f42e1e1831..6074398ec2 100644 --- a/src/libprs500/gui2/main.py +++ b/src/libprs500/gui2/main.py @@ -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() \ No newline at end of file diff --git a/src/libprs500/gui2/main.ui b/src/libprs500/gui2/main.ui index 28ecbbde22..fad4e82626 100644 --- a/src/libprs500/gui2/main.ui +++ b/src/libprs500/gui2/main.ui @@ -42,7 +42,7 @@ 0 - + 0 @@ -90,7 +90,7 @@ - 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 + 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 Qt::RichText @@ -332,11 +332,6 @@ - - DeviceView - QListView -
widgets.h
-
BooksView QTableView @@ -347,6 +342,11 @@ QLineEdit
library.h
+ + LocationView + QListView +
widgets.h
+
diff --git a/src/libprs500/gui2/main_ui.py b/src/libprs500/gui2/main_ui.py index be461a3833..d424edc1bc 100644 --- a/src/libprs500/gui2/main_ui.py +++ b/src/libprs500/gui2/main_ui.py @@ -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 http://libprs500.kovidgoyal.net

libprs500: %1 by Kovid Goyal © 2006
%2 %3 %4", None, QtGui.QApplication.UnicodeUTF8)) + self.df.setText(QtGui.QApplication.translate("MainWindow", "For help visit http://libprs500.kovidgoyal.net

libprs500: %1 by Kovid Goyal © 2007
%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, author, publisher, tags and comments

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 diff --git a/src/libprs500/gui2/progress.py b/src/libprs500/gui2/progress.py deleted file mode 100644 index 5c0bb27cfb..0000000000 --- a/src/libprs500/gui2/progress.py +++ /dev/null @@ -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() - \ No newline at end of file diff --git a/src/libprs500/gui2/widgets.py b/src/libprs500/gui2/widgets.py index a23d47eb9c..0aa4f39b24 100644 --- a/src/libprs500/gui2/widgets.py +++ b/src/libprs500/gui2/widgets.py @@ -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 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)) + 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() +