mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
lit2lrf
Basic GUI2 job control
This commit is contained in:
parent
6c63fc0ec7
commit
8989b541f1
@ -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'
|
||||
|
@ -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).
|
||||
"""
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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.''')
|
||||
|
@ -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'):
|
||||
|
15
src/libprs500/ebooks/lrf/lit/__init__.py
Normal file
15
src/libprs500/ebooks/lrf/lit/__init__.py
Normal 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.
|
||||
|
96
src/libprs500/ebooks/lrf/lit/convert_from.py
Normal file
96
src/libprs500/ebooks/lrf/lit/convert_from.py
Normal 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()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
BIN
src/libprs500/gui2/images/jobs-animated.mng
Normal file
BIN
src/libprs500/gui2/images/jobs-animated.mng
Normal file
Binary file not shown.
84
src/libprs500/gui2/jobs.py
Normal file
84
src/libprs500/gui2/jobs.py
Normal 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()
|
||||
|
@ -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()
|
@ -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 <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</string>
|
||||
<string>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</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" />
|
||||
|
@ -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> © 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> © 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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user