Support for embedded fonts in html2lrf. Fix title sorting of books on device. Lots of progress in gui2.

This commit is contained in:
Kovid Goyal 2007-06-29 00:16:14 +00:00
parent fe2e72d699
commit 4f38f13271
18 changed files with 640 additions and 193 deletions

View File

@ -102,7 +102,7 @@ class Device(object):
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).
title, authors, size, datetime (a UTC time tuple), path, thumbnail (can be None).
"""
raise NotImplementedError()
@ -133,3 +133,11 @@ class Device(object):
"""
raise NotImplementedError()
def sync_booklists(self, booklists, end_session=True):
'''
Update metadata on device.
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
'''
raise NotImplementedError()

View File

@ -19,7 +19,7 @@ in the reader cache.
import xml.dom.minidom as dom
from base64 import b64decode as decode
from base64 import b64encode as encode
import time
import time, re
MIME_MAP = { \
"lrf":"application/x-sony-bbeb", \
@ -28,6 +28,9 @@ MIME_MAP = { \
"txt":"text/plain" \
}
def sortable_title(title):
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', ' An Crum').rstrip()
class book_metadata_field(object):
""" Represents metadata stored as an attribute """
def __init__(self, attr, formatter=None, setter=None):
@ -48,7 +51,7 @@ class book_metadata_field(object):
class Book(object):
""" Provides a view onto the XML element that represents a book """
title = book_metadata_field("title")
author = book_metadata_field("author", \
authors = book_metadata_field("author", \
formatter=lambda x: x if x and x.strip() else "Unknown")
mime = book_metadata_field("mime")
rpath = book_metadata_field("path")
@ -60,6 +63,18 @@ class Book(object):
formatter=lambda x: time.strptime(x.strip(), "%a, %d %b %Y %H:%M:%S %Z"),
setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(x)))
@apply
def title_sorter():
doc = '''String to sort the title. If absent, title is returned'''
def fget(self):
src = self.elem.getAttribute('titleSorter').strip()
if not src:
src = self.title
return src
def fset(self, val):
self.elem.setAttribute('titleSorter', sortable_title(str(val)))
return property(doc=doc, fget=fget, fset=fset)
@apply
def thumbnail():
doc = \
@ -186,6 +201,7 @@ class BookList(list):
sourceid = str(self[0].sourceid) if len(self) else "1"
attrs = {
"title" : info["title"],
'titleSorter' : info['title'],
"author" : info["authors"] if info['authors'] else 'Unknown', \
"page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \

View File

@ -813,6 +813,15 @@ class PRS500(Device):
for path in paths:
self.del_file(path, end_session=False)
fix_ids(booklists[0], booklists[1])
self.sync_booklists(booklists, end_session=False)
@safe
def sync_booklists(self, booklists, end_session=True):
'''
Upload bookslists to device.
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=False), L{books}(oncard=True)).
'''
self.upload_book_list(booklists[0], end_session=False)
if len(booklists[1]):
self.upload_book_list(booklists[1], end_session=False)
@ -858,9 +867,7 @@ class PRS500(Device):
bkl.add_book(info, name, size, ctime)
fix_ids(booklists[0], booklists[1])
if sync_booklists:
self.upload_book_list(booklists[0], end_session=False)
if len(booklists[1]):
self.upload_book_list(booklists[1], end_session=False)
self.sync_booklists(booklists, end_session=False)
@safe
def upload_book_list(self, booklist, end_session=True):

View File

@ -16,13 +16,17 @@
This package contains logic to read and write LRF files.
The LRF file format is documented at U{http://www.sven.de/librie/Librie/LrfFormat}.
"""
import sys, os
from optparse import OptionParser, OptionValueError
from ttfquery import describe, findsystem
from fontTools.ttLib import TTLibError
from libprs500.ebooks.lrf.pylrs.pylrs import Book as _Book
from libprs500.ebooks.lrf.pylrs.pylrs import TextBlock, Header, PutObj, \
Paragraph, TextStyle, BlockStyle
from libprs500.ebooks.lrf.fonts import FONT_FILE_MAP
from libprs500 import __version__ as VERSION
from libprs500 import iswindows
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
@ -33,11 +37,13 @@ class PRS500_PROFILE(object):
dpi = 166
# Number of pixels to subtract from screen_height when calculating height of text area
fudge = 18
font_size = 10 #: Default (in pt)
parindent = 80 #: Default (in px)
line_space = 1.2 #: Default (in pt)
font_size = 10 #: Default (in pt)
parindent = 80 #: Default (in px)
line_space = 1.2 #: Default (in pt)
header_font_size = 6 #: In pt
header_height = 30 #: In px
default_fonts = { 'sans': "Swis721 BT Roman", 'mono': "Courier10 BT Roman",
'serif': "Dutch801 Rm BT Roman"}
@ -47,6 +53,20 @@ def profile_from_string(option, opt_str, value, parser):
else:
raise OptionValueError('Profile: '+value+' is not implemented')
def font_family(option, opt_str, value, parser):
if value:
value = value.split(',')
if len(value) != 2:
raise OptionValueError('Font family specification must be of the form'+\
' "path to font directory, font family"')
path, family = tuple(value)
if not os.path.isdir(path) or not os.access(path, os.R_OK|os.X_OK):
raise OptionValueError('Cannot read from ' + path)
setattr(parser.values, option.dest, (path, family))
else:
setattr(parser.values, option.dest, tuple())
class ConversionError(Exception):
pass
@ -98,6 +118,24 @@ def option_parser(usage):
page.add_option('--bottom-margin', default=0, dest='bottom_margin', type='int',
help='''Bottom margin of page. Default is %default px.''')
fonts = parser.add_option_group('FONT FAMILIES',
'''Specify trutype font families for serif, sans-serif and monospace fonts. '''
'''These fonts will be embedded in the LRF file. Note that custom fonts lead to '''
'''slower page turns. Each family specification is of the form: '''
'''"path to fonts directory, family" '''
'''For example: '''
'''--serif-family "%s, Times New Roman"
''' % ('C:\Windows\Fonts' if iswindows else '/usr/share/fonts/corefonts'))
fonts.add_option('--serif-family', action='callback', callback=font_family,
default=None, dest='serif_family', type='string',
help='The serif family of fonts to embed')
fonts.add_option('--sans-family', action='callback', callback=font_family,
default=None, dest='sans_family', type='string',
help='The sans-serif family of fonts to embed')
fonts.add_option('--mono-family', action='callback', callback=font_family,
default=None, dest='mono_family', type='string',
help='The monospace family of fonts to embed')
debug = parser.add_option_group('DEBUG OPTIONS')
debug.add_option('--verbose', dest='verbose', action='store_true', default=False,
help='''Be verbose while processing''')
@ -105,6 +143,42 @@ def option_parser(usage):
help='Convert to LRS', default=False)
return parser
def find_custom_fonts(options):
fonts = {'serif' : None, 'sans' : None, 'mono' : None}
def find_family(option):
path, family = option
paths = findsystem.findFonts([path])
results = {}
for path in paths:
if len(results.keys()) == 4:
break
f = describe.openFont(path)
name, cfamily = describe.shortName(f)
if cfamily.lower().strip() != family.lower().strip():
continue
try:
wt, italic = describe.modifiers(f)
except TTLibError:
print >>sys.stderr, 'Could not process', path
result = (path, name)
if wt == 400 and italic == 0:
results['normal'] = result
elif wt == 400 and italic > 0:
results['italic'] = result
elif wt >= 700 and italic == 0:
results['bold'] = result
elif wt >= 700 and italic > 0:
results['bi'] = result
return results
if options.serif_family:
fonts['serif'] = find_family(options.serif_family)
if options.sans_family:
fonts['sans'] = find_family(options.sans_family)
if options.mono_family:
fonts['mono'] = find_family(options.mono_family)
return fonts
def Book(options, font_delta=0, header=None,
profile=PRS500_PROFILE, **settings):
ps = {}
@ -126,9 +200,27 @@ def Book(options, font_delta=0, header=None,
ps['textheight'] = profile.screen_height - (options.bottom_margin + ps['topmargin'] + ps['headheight'] + profile.fudge)
fontsize = int(10*profile.font_size+font_delta*20)
baselineskip = fontsize + 20
return _Book(textstyledefault=dict(fontsize=fontsize,
parindent=int(profile.parindent),
linespace=int(10*profile.line_space),
baselineskip=baselineskip), \
pagestyledefault=ps, blockstyledefault=dict(blockwidth=ps['textwidth']),
**settings)
fonts = find_custom_fonts(options)
tsd = dict(fontsize=fontsize,
parindent=int(profile.parindent),
linespace=int(10*profile.line_space),
baselineskip=baselineskip)
if fonts['serif'] and fonts['serif'].has_key('normal'):
tsd['fontfacename'] = fonts['serif']['normal'][1]
book = _Book(textstyledefault=tsd,
pagestyledefault=ps,
blockstyledefault=dict(blockwidth=ps['textwidth']),
**settings)
for family in fonts.keys():
if fonts[family]:
for font in fonts[family].values():
book.embed_font(*font)
FONT_FILE_MAP[font[1]] = font[0]
for family in ['serif', 'sans', 'mono']:
if not fonts[family]:
fonts[family] = { 'normal' : (None, profile.default_fonts[family]) }
elif not fonts[family].has_key('normal'):
raise ConversionError, 'Could not find the normal version of the ' + family + ' font'
return book, fonts

View File

@ -56,4 +56,5 @@ def get_font(name, size, encoding='unic'):
if name in FONT_MAP.keys():
path = get_font_path(name)
return ImageFont.truetype(path, size, encoding=encoding)
elif name in FONT_FILE_MAP.keys():
return ImageFont.truetype(FONT_FILE_MAP[name], size, encoding=encoding)

View File

@ -81,29 +81,46 @@ class Span(_Span):
return result
@staticmethod
def translate_attrs(d, dpi, font_delta=0, memory=None):
def translate_attrs(d, dpi, fonts, font_delta=0, memory=None):
"""
Receives a dictionary of html attributes and styles and returns
approximate Xylog equivalents in a new dictionary
"""
def font_weight(val):
ans = None
ans = 0
m = re.search("([0-9]+)", val)
if m:
ans = str(int(m.group(1)))
ans = int(m.group(1))
elif val.find("bold") >= 0 or val.find("strong") >= 0:
ans = "1000"
ans = 700
return 'bold' if ans >= 700 else 'normal'
def font_style(val):
ans = 'normal'
if 'italic' in val or 'oblique' in val:
ans = 'italic'
return ans
def font_family(val):
ans = None
ans = 'serif'
if max(val.find("courier"), val.find("mono"), val.find("fixed"), val.find("typewriter"))>=0:
ans = "Courier10 BT Roman"
ans = 'mono'
elif max(val.find("arial"), val.find("helvetica"), val.find("verdana"),
val.find("trebuchet"), val.find("sans")) >= 0:
ans = "Swis721 BT Roman"
ans = 'sans'
return ans
def font_key(family, style, weight):
key = 'normal'
if style == 'italic' and weight == 'normal':
key = 'italic'
elif style == 'normal' and weight == 'bold':
key = 'bold'
elif style == 'italic' and weight == 'bold':
key = 'bi'
return key
def font_size(val):
ans = None
unit = Span.unit_convert(val, dpi, 14)
@ -129,37 +146,38 @@ class Span(_Span):
return ans
t = dict()
family, weight, style = 'serif', 'normal', 'normal'
for key in d.keys():
val = d[key].lower()
if key == 'font':
val = val.split()
val.reverse()
for sval in val:
ans = font_family(sval)
if ans:
t['fontfacename'] = ans
else:
ans = font_size(sval)
if ans:
t['fontsize'] = ans
else:
ans = font_weight(sval)
if ans:
t['fontweight'] = ans
vals = val.split()
for val in vals:
family = font_family(val)
if family != 'serif':
break
for val in vals:
weight = font_weight(val)
if weight != 'normal':
break
for val in vals:
style = font_style(val)
if style != 'normal':
break
for val in vals:
sz = font_size(val)
if sz:
t['fontsize'] = sz
break
elif key in ['font-family', 'font-name']:
ans = font_family(val)
if ans:
t['fontfacename'] = ans
family = font_family(val)
elif key == "font-size":
ans = font_size(val)
if ans:
t['fontsize'] = ans
elif key == 'font-weight':
ans = font_weight(val)
if ans:
t['fontweight'] = ans
if int(ans) > 140:
t['wordspace'] = '50'
weight = font_weight(val)
elif key == 'font-style':
style = font_style(val)
else:
report = True
if memory != None:
@ -169,22 +187,32 @@ class Span(_Span):
memory.append(key)
if report:
print >>sys.stderr, 'Unhandled/malformed CSS key:', key, d[key]
t['fontfacename'] = (family, font_key(family, style, weight))
if t.has_key('fontsize') and int(t['fontsize']) > 120:
t['wordspace'] = 50
return t
def __init__(self, ns, css, memory, dpi, font_delta=0):
def __init__(self, ns, css, memory, dpi, fonts, font_delta=0):
src = ns.string if hasattr(ns, 'string') else ns
src = re.sub(r'\s{2,}', ' ', src) # Remove multiple spaces
for pat, repl in Span.rules:
src = pat.sub(repl, src)
if not src:
raise ConversionError('No point in adding an empty string to a Span')
if 'font-style' in css.keys():
fs = css.pop('font-style')
if fs.lower() == 'italic':
attrs = Span.translate_attrs(css, dpi, fonts, font_delta=font_delta, memory=memory)
family, key = attrs['fontfacename']
if fonts[family].has_key(key):
attrs['fontfacename'] = fonts[family][key][1]
else:
attrs['fontfacename'] = fonts[family]['normal'][1]
if key in ['bold', 'bi']:
attrs['fontweight'] = 700
if key in ['italic', 'bi']:
src = Italic(src)
attrs = Span.translate_attrs(css, dpi, font_delta=font_delta, memory=memory)
if 'fontsize' in attrs.keys():
attrs['baselineskip'] = int(attrs['fontsize']) + 20
if attrs['fontfacename'] == fonts['serif']['normal'][1]:
attrs.pop('fontfacename')
_Span.__init__(self, text=src, **attrs)
@ -214,7 +242,7 @@ class HTMLConverter(object):
processed_files = {} #: Files that have been processed
def __init__(self, book, path,
def __init__(self, book, fonts, path,
font_delta=0, verbose=False, cover=None,
max_link_levels=sys.maxint, link_level=0,
is_root=True, baen=False, chapter_detection=True,
@ -231,6 +259,7 @@ class HTMLConverter(object):
@param book: The LRF book
@type book: L{libprs500.lrf.pylrs.Book}
@param fonts: dict specifying the font families to use
@param path: path to the HTML file to process
@type path: C{str}
@param width: Width of the device on which the LRF file is to be read
@ -278,6 +307,7 @@ class HTMLConverter(object):
th = {'font-size' : 'large', 'font-weight':'bold'},
big = {'font-size' : 'large', 'font-weight':'bold'},
)
self.fonts = fonts #: dict specifting font families to use
self.profile = profile #: Defines the geometry of the display device
self.chapter_detection = chapter_detection #: Flag to toggle chapter detection
self.chapter_regex = chapter_regex #: Regex used to search for chapter titles
@ -553,7 +583,8 @@ class HTMLConverter(object):
path = os.path.abspath(path)
if not path in HTMLConverter.processed_files.keys():
try:
self.files[path] = HTMLConverter(self.book, path,
self.files[path] = HTMLConverter(
self.book, self.fonts, path,
profile=self.profile,
font_delta=self.font_delta, verbose=self.verbose,
link_level=self.link_level+1,
@ -690,7 +721,7 @@ class HTMLConverter(object):
self.process_alignment(css)
try:
self.current_para.append(Span(src, self.sanctify_css(css), self.memory,\
self.profile.dpi, font_delta=self.font_delta))
self.profile.dpi, self.fonts, font_delta=self.font_delta))
except ConversionError, err:
if self.verbose:
print >>sys.stderr, err
@ -949,7 +980,8 @@ class HTMLConverter(object):
elif tagname == 'pre':
self.end_current_para()
self.current_block.append_to(self.current_page)
attrs = Span.translate_attrs(tag_css, self.profile.dpi, self.font_delta, self.memory)
attrs = Span.translate_attrs(tag_css, self.profile.dpi, self.fonts, self.font_delta, self.memory)
attrs['fontfacename'] = self.fonts['mono']['normal'][1]
ts = self.book.create_text_style(**self.unindented_style.attrs)
ts.attrs.update(attrs)
self.current_block = self.book.create_text_block(
@ -959,7 +991,7 @@ class HTMLConverter(object):
lines = src.split('\n')
for line in lines:
try:
self.current_para.append(Span(line, tag_css, self.memory, self.profile.dpi))
self.current_para.append(Span(line, tag_css, self.memory, self.profile.dpi, self.fonts))
self.current_para.CR()
except ConversionError:
pass
@ -1145,14 +1177,14 @@ def process_file(path, options):
header.append(Bold(options.title))
header.append(' by ')
header.append(Italic(options.author+" "))
book = Book(options, header=header, **args)
book, fonts = Book(options, header=header, **args)
le = re.compile(options.link_exclude) if options.link_exclude else \
re.compile('$')
pb = re.compile(options.page_break, re.IGNORECASE) if options.page_break else \
re.compile('$')
fpb = re.compile(options.force_page_break, re.IGNORECASE) if options.force_page_break else \
re.compile('$')
conv = HTMLConverter(book, path, profile=options.profile,
conv = HTMLConverter(book, fonts, path, profile=options.profile,
font_delta=options.font_delta,
cover=cpath, max_link_levels=options.link_levels,
verbose=options.verbose, baen=options.baen,

View File

@ -8,7 +8,7 @@
</head>
<h1>Demo of <span style='font-family:monospace'>html2lrf</span></h1>
<p>
This file contains a demonstration of the capabilities of <span style='font-family:monospace'>html2lrf,</span> the HTML to LRF converter from <em>libprs500.</em> To obtain libprs500 visit<br/><span style='font:sans-serif'>https://libprs500.kovidgoyal.net</span>
This file contains a demonstration of the capabilities of <span style='font-family:monospace'>html2lrf</span>, the HTML to LRF converter from <em>libprs500.</em> To obtain libprs500 visit<br/><span style='font:sans-serif'>https://libprs500.kovidgoyal.net</span>
</p>
<br/>
<h2><a name='toc'>Table of Contents</a></h2>
@ -17,18 +17,21 @@
<li><a href='#tables'>Tables</a></li>
<li><a href='#text'>Text formatting and ruled lines</a></li>
<li><a href='#images'>Inline images</a></li>
<li><a href='#fonts'>Embedded Fonts</a></li>
<li><a href='#recursive'>Recursive link following</a></li>
<li><a href='demo_ext.html'>The HTML used to create this file</a>
</ul>
<h2><a name='lists'>Lists</a></h2>
<p><h3>Unordered lists</h3>
<p></p>
<h3>Unordered lists</h3>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</p>
<p><h3>Ordered lists</h3>
<p></p>
<h3>Ordered lists</h3>
<ol>
<li>Item 1</li>
<li>Item 2</li>
@ -36,7 +39,7 @@
</p>
<br/>
<p>
Note that nested lists are not supported.
Note that nested lists are not supported.<br />
</p>
<p class='toc'>
<hr />
@ -94,7 +97,7 @@
<h2><a name='text'>Text formatting</a></h2>
<p>
A simple <i>paragraph</i> of <b>formatted
<i>text</i></b> with a ruled line following it.
<i>text</i></b>, with a ruled line following it.
</p>
<hr/>
<p> A
@ -113,7 +116,7 @@
<hr/>
<p style='text-indent:30em'>A very indented paragraph</p>
<p style='text-indent:0em'>An unindented paragraph</p>
<p>A default indented paragrpah</p>
<p>A default indented paragrpah<br /></p>
<p class='toc'>
<hr />
<a href='#toc'>Table of Contents</a>
@ -128,9 +131,29 @@
<a href='#toc'>Table of Contents</a>
</p>
<h2><a name='fonts'>Embedded fonts</a></h2>
<p>This LRF file has been prepared by embedding Times New Roman and Andale Mono
as the default serif and monospace fonts. This allows it to correctly display
non English characters such as: </p>
<ul>
<li>mouse in German: mūs</li>
<li>mouse in Russian: мышь</li>
</ul>
<p>
Note that embedding fonts in LRF files slows down page turns slightly.
<br />
</p>
<p class='toc'>
<hr />
<a href='#toc'>Table of Contents</a>
</p>
<h2><a name='recursive'>Recursive link following</a></h2>
<p>
<span style='font:monospace'>html2lrf</span> follows links in HTML files that point to other files, recursively. Thus it can be used to convert a whole tree of HTML files into a single LRF file.
<br />
</p>
<p class='toc'>
<hr />

View File

@ -503,6 +503,10 @@ class Book(Delegator):
if isinstance(candidate, Page):
return candidate
def embed_font(self, file, facename):
f = Font(file, facename)
self.append(f)
def getSettings(self):
return ["sourceencoding"]

View File

@ -700,7 +700,7 @@ class DeviceBooksModel(QAbstractTableModel):
if col == 0:
text = TableView.wrap(book.title, width=40)
elif col == 1:
au = book.author
au = book.authors
au = au.split("&")
jau = [ TableView.wrap(a, width=25).strip() for a in au ]
text = "\n".join(jau)
@ -723,7 +723,7 @@ class DeviceBooksModel(QAbstractTableModel):
cover = None if pix.isNull() else pix
except:
traceback.print_exc()
au = row.author if row.author else "Unknown"
au = row.authors if row.authors else "Unknown"
return row.title, au, TableView.human_readable(row.size), row.mime, cover
def sort(self, col, order):

View File

@ -75,8 +75,7 @@ class DeviceJob(QThread):
self.id, self.result, exception, last_traceback)
def progress_update(self, val):
print val
self.emit(SIGNAL('status_update(int)'), int(val), Qt.QueuedConnection)
self.emit(SIGNAL('status_update(int)'), int(val))
class DeviceManager(QObject):
@ -85,7 +84,7 @@ class DeviceManager(QObject):
self.device_class = device_class
self.device = device_class()
def get_info_func(self):
def info_func(self):
''' Return callable that returns device information and free space on device'''
def get_device_information(updater):
self.device.set_progress_reporter(updater)
@ -102,5 +101,12 @@ class DeviceManager(QObject):
self.device.set_progress_reporter(updater)
mainlist = self.device.books(oncard=False, end_session=False)
cardlist = self.device.books(oncard=True)
return mainlist, cardlist
return (mainlist, cardlist)
return books
def sync_booklists_func(self):
'''Upload booklists to device'''
def sync_booklists(updater, booklists):
self.device.set_progress_reporter(updater)
self.device.sync_booklists(booklists)
return sync_booklists

View File

@ -39,6 +39,7 @@ class JobManager(QAbstractTableModel):
job = job_class(self.next_id, lock, *args, **kwargs)
QObject.connect(job, SIGNAL('finished()'), self.cleanup_jobs)
self.jobs[self.next_id] = job
self.emit(SIGNAL('job_added(int)'), self.next_id)
return job
finally:
self.job_create_lock.unlock()
@ -55,8 +56,9 @@ class JobManager(QAbstractTableModel):
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)
if slot:
QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
slot)
job.start()
def job_done(self, id, *args, **kwargs):
@ -69,6 +71,8 @@ class JobManager(QAbstractTableModel):
self.cleanup_lock.lock()
self.cleanup[id] = job
self.cleanup_lock.unlock()
if len(self.jobs.keys()) == 0:
self.emit(SIGNAL('no_more_jobs()'))
finally:
self.job_remove_lock.unlock()

View File

@ -13,7 +13,8 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import os, textwrap, traceback, time, re
from datetime import timedelta
from datetime import timedelta, datetime
from operator import attrgetter
from math import cos, sin, pi
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
@ -77,66 +78,6 @@ class LibraryDelegate(QItemDelegate):
traceback.print_exc(e)
painter.restore()
class BooksView(QTableView):
wrapper = textwrap.TextWrapper(width=20)
@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 __init__(self, parent):
QTableView.__init__(self, parent)
self.display_parent = parent
self.model = BooksModel(self)
self.setModel(self.model)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True)
self.setItemDelegateForColumn(4, LibraryDelegate(self))
QObject.connect(self.model, SIGNAL('sorted()'), self.resizeRowsToContents)
QObject.connect(self.model, SIGNAL('searched()'), self.resizeRowsToContents)
self.verticalHeader().setVisible(False)
def set_database(self, db):
self.model.set_database(db)
def migrate_database(self):
if self.model.database_needs_migration():
print 'Migrating database from pre 0.4.0 version'
path = os.path.abspath(os.path.expanduser('~/library.db'))
progress = QProgressDialog('Upgrading database from pre 0.4.0 version.<br>'+\
'The new database is stored in the file <b>'+self.model.db.dbpath,
QString(), 0, LibraryDatabase.sizeof_old_database(path),
self)
progress.setModal(True)
app = QCoreApplication.instance()
def meter(count):
progress.setValue(count)
app.processEvents()
progress.setWindowTitle('Upgrading database')
progress.show()
LibraryDatabase.import_old_database(path, self.model.db.conn, meter)
def connect_to_search_box(self, sb):
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.model.search)
class BooksModel(QAbstractTableModel):
def __init__(self, parent):
@ -150,7 +91,7 @@ class BooksModel(QAbstractTableModel):
db = LibraryDatabase(os.path.expanduser(str(db)))
self.db = db
def search(self, text, refinement):
def search_tokens(self, text):
tokens = []
quot = re.search('"(.*?)"', text)
while quot:
@ -158,6 +99,10 @@ class BooksModel(QAbstractTableModel):
text = text.replace('"'+quot.group(1)+'"', '')
quot = re.search('"(.*?)"', text)
tokens += text.split(' ')
return [re.compile(i, re.IGNORECASE) for i in tokens]
def search(self, text, refinement):
tokens = self.search_tokens(text)
self.db.filter(tokens, refinement)
self.reset()
self.emit(SIGNAL('searched()'))
@ -204,7 +149,7 @@ class BooksModel(QAbstractTableModel):
dt = self.db.timestamp(row)
if dt:
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QVariant(dt.strftime('%d %b %Y'))
return QVariant(dt.strftime(BooksView.TIME_FMT))
elif col == 4:
r = self.db.rating(row)
r = r/2 if r else 0
@ -218,11 +163,7 @@ class BooksModel(QAbstractTableModel):
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 QVariant("Double click to <b>edit</b> me<br><br>")
return NONE
def headerData(self, section, orientation, role):
@ -232,13 +173,13 @@ class BooksModel(QAbstractTableModel):
if orientation == Qt.Horizontal:
if section == 0: text = "Title"
elif section == 1: text = "Author(s)"
elif section == 2: text = "Size"
elif section == 2: text = "Size (MB)"
elif section == 3: text = "Date"
elif section == 4: text = "Rating"
elif section == 5: text = "Publisher"
return QVariant(self.trUtf8(text))
else:
return NONE
return QVariant(section+1)
def flags(self, index):
flags = QAbstractTableModel.flags(self, index)
@ -267,10 +208,201 @@ class BooksModel(QAbstractTableModel):
done = True
return done
class BooksView(QTableView):
TIME_FMT = '%d %b %Y'
wrapper = textwrap.TextWrapper(width=20)
@classmethod
def wrap(cls, s, width=20):
cls.wrapper.width = width
return cls.wrapper.fill(s)
@classmethod
def human_readable(cls, size, precision=1):
""" Convert a size in bytes into megabytes """
return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),)
def __init__(self, parent, modelcls=BooksModel):
QTableView.__init__(self, parent)
self.display_parent = parent
self._model = modelcls(self)
self.setModel(self._model)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True)
self.setItemDelegateForColumn(4, LibraryDelegate(self))
QObject.connect(self._model, SIGNAL('sorted()'), self.resizeRowsToContents)
QObject.connect(self._model, SIGNAL('searched()'), self.resizeRowsToContents)
#self.verticalHeader().setVisible(False)
def set_database(self, db):
self._model.set_database(db)
def migrate_database(self):
if self._model.database_needs_migration():
print 'Migrating database from pre 0.4.0 version'
path = os.path.abspath(os.path.expanduser('~/library.db'))
progress = QProgressDialog('Upgrading database from pre 0.4.0 version.<br>'+\
'The new database is stored in the file <b>'+self._model.db.dbpath,
QString(), 0, LibraryDatabase.sizeof_old_database(path),
self)
progress.setModal(True)
app = QCoreApplication.instance()
def meter(count):
progress.setValue(count)
app.processEvents()
progress.setWindowTitle('Upgrading database')
progress.show()
LibraryDatabase.import_old_database(path, self._model.db.conn, meter)
def connect_to_search_box(self, sb):
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self._model.search)
class DeviceBooksView(BooksView):
def __init__(self, parent):
BooksView.__init__(self, parent, DeviceBooksModel)
self.columns_resized = False
def resizeColumnsToContents(self):
QTableView.resizeColumnsToContents(self)
self.columns_resized = True
def connect_dirtied_signal(self, slot):
QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot)
class DeviceBooksModel(BooksModel):
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
self.db = []
self.map = []
self.sorted_map = []
self.unknown = str(self.trUtf8('Unknown'))
def search(self, text, refinement):
tokens = self.search_tokens(text)
base = self.map if refinement else self.sorted_map
result = []
for i in base:
add = True
q = self.db[i].title + ' ' + self.db[i].authors
for token in tokens:
if not token.search(q):
add = False
break
if add:
result.append(i)
self.map = result
self.reset()
self.emit(SIGNAL('searched()'))
def sort(self, col, order):
if not self.db:
return
descending = order != Qt.AscendingOrder
def strcmp(attr):
ag = attrgetter(attr)
def _strcmp(x, y):
x = ag(self.db[x])
y = ag(self.db[y])
if x == None:
x = ''
if y == None:
y = ''
x, y = x.strip().lower(), y.strip().lower()
return cmp(x, y)
return _strcmp
def datecmp(x, y):
x = self.db[x].datetime
y = self.db[y].datetime
return cmp(datetime(*x[0:6]), datetime(*y[0:6]))
def sizecmp(x, y):
x, y = int(self.db[x].size), int(self.db[y].size)
return cmp(x, y)
fcmp = strcmp('title_sorter') if col == 0 else strcmp('authors') if col == 1 else \
sizecmp if col == 2 else datecmp
self.map.sort(cmp=fcmp, reverse=descending)
if len(self.map) == len(self.db):
self.sorted_map = list(self.map)
else:
self.sorted_map = list(range(len(self.db)))
self.sorted_map.sort(cmp=fcmp, reverse=descending)
self.sorted_on = (col, order)
self.reset()
self.emit(SIGNAL('sorted()'))
def columnCount(self, parent):
return 4
def rowCount(self, parent):
return len(self.map)
def set_database(self, db):
self.db = db
self.map = list(range(0, len(db)))
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column()
if col == 0:
text = self.db[self.map[row]].title
if not text:
text = self.unknown
return QVariant(BooksView.wrap(text, width=35))
elif col == 1:
au = self.db[self.map[row]].authors
if not au:
au = self.unknown
if role == Qt.EditRole:
return QVariant(au)
au = au.split(',')
authors = []
for i in au:
authors += i.strip().split('&')
jau = [ BooksView.wrap(a.strip(), width=30).strip() for a in authors ]
return QVariant("\n".join(jau))
elif col == 2:
size = self.db[self.map[row]].size
return QVariant(BooksView.human_readable(size))
elif col == 3:
dt = self.db[self.map[row]].datetime
dt = datetime(*dt[0:6])
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QVariant(dt.strftime(BooksView.TIME_FMT))
elif role == Qt.TextAlignmentRole and index.column() in [2, 3]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter)
elif role == Qt.ToolTipRole and index.isValid():
if index.column() in [0, 1]:
return QVariant("Double click to <b>edit</b> me<br><br>")
return NONE
def setData(self, index, value, role):
done = False
if role == Qt.EditRole:
row, col = index.row(), index.column()
if col in [2, 3]:
return False
val = unicode(value.toString().toUtf8(), 'utf-8').strip()
idx = self.map[row]
if col == 0:
self.db.title = val
self.db.title_sorter = val
elif col == 1:
self.db.authors = val
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
index, index)
self.emit(SIGNAL('booklist_dirtied()'))
if col == self.sorted_on[0]:
self.sort(col, self.sorted_on[1])
done = True
return done
class SearchBox(QLineEdit):
def __init__(self, parent):
QLineEdit.__init__(self, parent)
self.setText('Search by title, author, publisher, tags and comments')
self.help_text = 'Search by title, author, publisher, tags and comments'
self.setText(self.help_text)
self.home(False)
QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot)
self.default_palette = QApplication.palette(self)
@ -282,13 +414,22 @@ class SearchBox(QLineEdit):
self.timer = None
self.interval = 1000 #: Time to wait before emitting search signal
def normalize_state(self):
self.setText('')
self.setPalette(self.default_palette)
def keyPressEvent(self, event):
if self.initial_state:
self.setText('')
self.normalize_state()
self.initial_state = False
self.setPalette(self.default_palette)
QLineEdit.keyPressEvent(self, event)
def mouseReleaseEvent(self, event):
if self.initial_state:
self.normalize_state()
self.initial_state = False
QLineEdit.mouseReleaseEvent(self, event)
def text_edited_slot(self, text):
text = str(text)
self.prev_text = text

View File

@ -15,11 +15,8 @@
import os, tempfile, sys
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QSettings, QVariant, QSize, QEventLoop, QString, \
QBuffer, QIODevice, QModelIndex, QThread
from PyQt4.QtGui import QPixmap, QErrorMessage, QLineEdit, \
QMessageBox, QFileDialog, QIcon, QDialog, QInputDialog
from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical
QSettings, QVariant, QSize, QThread
from PyQt4.QtGui import QErrorMessage
from libprs500 import __version__ as VERSION
from libprs500.gui2 import APP_TITLE, installErrorHandler
@ -40,6 +37,10 @@ class Main(QObject, Ui_MainWindow):
self.device_manager = None
self.temporary_slots = {}
####################### Location View ########################
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
self.location_selected)
####################### Vanity ########################
self.vanity_template = self.vanity.text().arg(VERSION)
self.vanity.setText(self.vanity_template.arg(' '))
@ -47,10 +48,17 @@ class Main(QObject, Ui_MainWindow):
####################### Status Bar #####################
self.status_bar = StatusBar()
self.window.setStatusBar(self.status_bar)
QObject.connect(self.job_manager, SIGNAL('job_added(int)'), self.status_bar.job_added)
QObject.connect(self.job_manager, SIGNAL('no_more_jobs()'), self.status_bar.no_more_jobs)
####################### Setup books view ########################
####################### Library view ########################
self.library_view.set_database(self.database_path)
self.library_view.connect_to_search_box(self.search)
self.memory_view.connect_to_search_box(self.search)
self.card_view.connect_to_search_box(self.search)
self.memory_view.connect_dirtied_signal(self.upload_booklists)
self.card_view.connect_dirtied_signal(self.upload_booklists)
window.closeEvent = self.close_event
window.show()
@ -67,16 +75,28 @@ class Main(QObject, Ui_MainWindow):
self.detector.start(QThread.InheritPriority)
def upload_booklists(self):
booklists = self.memory_view.model().db, self.card_view.model().db
self.job_manager.run_device_job(None, self.device_manager.sync_booklists_func(),
booklists)
def location_selected(self, location):
page = 0 if location == 'library' else 1 if location == 'main' else 2
self.stack.setCurrentIndex(page)
if page == 1:
self.memory_view.resizeRowsToContents()
self.memory_view.resizeColumnsToContents()
if page == 2:
self.card_view.resizeRowsToContents()
self.card_view.resizeColumnsToContents()
def job_exception(self, id, exception, formatted_traceback):
raise JobException, str(exception) + '\n\r' + formatted_traceback
def device_detected(self, cls, connected):
if connected:
self.device_manager = DeviceManager(cls)
func = self.device_manager.get_info_func()
func = self.device_manager.info_func()
self.job_manager.run_device_job(self.info_read, func)
def info_read(self, id, result, exception, formatted_traceback):
@ -86,6 +106,20 @@ class Main(QObject, Ui_MainWindow):
info, cp, fs = result
self.location_view.model().update_devices(cp, fs)
self.vanity.setText(self.vanity_template.arg('Connected '+' '.join(info[:-1])))
func = self.device_manager.books_func()
self.job_manager.run_device_job(self.books_read, func)
def books_read(self, id, result, exception, formatted_traceback):
if exception:
self.job_exception(id, exception, formatted_traceback)
return
mainlist, cardlist = result
self.memory_view.set_database(mainlist)
self.card_view.set_database(cardlist)
self.memory_view.sortByColumn(3, Qt.DescendingOrder)
self.card_view.sortByColumn(3, Qt.DescendingOrder)
self.location_selected('main')
def read_settings(self):

View File

@ -170,7 +170,7 @@
</layout>
</item>
<item row="2" column="0" >
<widget class="QStackedWidget" name="stacks" >
<widget class="QStackedWidget" name="stack" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
@ -178,7 +178,7 @@
</sizepolicy>
</property>
<property name="currentIndex" >
<number>0</number>
<number>2</number>
</property>
<widget class="QWidget" name="library" >
<layout class="QVBoxLayout" >
@ -218,7 +218,42 @@
<widget class="QWidget" name="main_memory" >
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="BooksView" name="main_memory_view" >
<widget class="DeviceBooksView" name="memory_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid" >
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page" >
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="DeviceBooksView" name="card_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
@ -347,6 +382,11 @@
<extends>QListView</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>DeviceBooksView</class>
<extends>QTableView</extends>
<header>library.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="images.qrc" />

View File

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'main.ui'
#
# Created: Fri Jun 22 16:40:15 2007
# Created: Wed Jun 27 16:19:53 2007
# by: PyQt4 UI code generator 4-snapshot-20070606
#
# WARNING! All changes made in this file will be lost!
@ -86,14 +86,14 @@ class Ui_MainWindow(object):
self.hboxlayout1.addWidget(self.clear_button)
self.gridlayout.addLayout(self.hboxlayout1,1,0,1,1)
self.stacks = QtGui.QStackedWidget(self.centralwidget)
self.stack = QtGui.QStackedWidget(self.centralwidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(100)
sizePolicy.setVerticalStretch(100)
sizePolicy.setHeightForWidth(self.stacks.sizePolicy().hasHeightForWidth())
self.stacks.setSizePolicy(sizePolicy)
self.stacks.setObjectName("stacks")
sizePolicy.setHeightForWidth(self.stack.sizePolicy().hasHeightForWidth())
self.stack.setSizePolicy(sizePolicy)
self.stack.setObjectName("stack")
self.library = QtGui.QWidget()
self.library.setObjectName("library")
@ -117,7 +117,7 @@ class Ui_MainWindow(object):
self.library_view.setShowGrid(False)
self.library_view.setObjectName("library_view")
self.vboxlayout.addWidget(self.library_view)
self.stacks.addWidget(self.library)
self.stack.addWidget(self.library)
self.main_memory = QtGui.QWidget()
self.main_memory.setObjectName("main_memory")
@ -125,24 +125,48 @@ class Ui_MainWindow(object):
self.gridlayout1 = QtGui.QGridLayout(self.main_memory)
self.gridlayout1.setObjectName("gridlayout1")
self.main_memory_view = BooksView(self.main_memory)
self.memory_view = DeviceBooksView(self.main_memory)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(100)
sizePolicy.setVerticalStretch(10)
sizePolicy.setHeightForWidth(self.main_memory_view.sizePolicy().hasHeightForWidth())
self.main_memory_view.setSizePolicy(sizePolicy)
self.main_memory_view.setAcceptDrops(True)
self.main_memory_view.setDragEnabled(True)
self.main_memory_view.setDragDropOverwriteMode(False)
self.main_memory_view.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.main_memory_view.setAlternatingRowColors(True)
self.main_memory_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.main_memory_view.setShowGrid(False)
self.main_memory_view.setObjectName("main_memory_view")
self.gridlayout1.addWidget(self.main_memory_view,0,0,1,1)
self.stacks.addWidget(self.main_memory)
self.gridlayout.addWidget(self.stacks,2,0,1,1)
sizePolicy.setHeightForWidth(self.memory_view.sizePolicy().hasHeightForWidth())
self.memory_view.setSizePolicy(sizePolicy)
self.memory_view.setAcceptDrops(True)
self.memory_view.setDragEnabled(True)
self.memory_view.setDragDropOverwriteMode(False)
self.memory_view.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.memory_view.setAlternatingRowColors(True)
self.memory_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.memory_view.setShowGrid(False)
self.memory_view.setObjectName("memory_view")
self.gridlayout1.addWidget(self.memory_view,0,0,1,1)
self.stack.addWidget(self.main_memory)
self.page = QtGui.QWidget()
self.page.setObjectName("page")
self.gridlayout2 = QtGui.QGridLayout(self.page)
self.gridlayout2.setObjectName("gridlayout2")
self.card_view = DeviceBooksView(self.page)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(100)
sizePolicy.setVerticalStretch(10)
sizePolicy.setHeightForWidth(self.card_view.sizePolicy().hasHeightForWidth())
self.card_view.setSizePolicy(sizePolicy)
self.card_view.setAcceptDrops(True)
self.card_view.setDragEnabled(True)
self.card_view.setDragDropOverwriteMode(False)
self.card_view.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.card_view.setAlternatingRowColors(True)
self.card_view.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.card_view.setShowGrid(False)
self.card_view.setObjectName("card_view")
self.gridlayout2.addWidget(self.card_view,0,0,1,1)
self.stack.addWidget(self.page)
self.gridlayout.addWidget(self.stack,2,0,1,1)
MainWindow.setCentralWidget(self.centralwidget)
self.tool_bar = QtGui.QToolBar(MainWindow)
@ -178,7 +202,7 @@ class Ui_MainWindow(object):
self.label.setBuddy(self.search)
self.retranslateUi(MainWindow)
self.stacks.setCurrentIndex(0)
self.stack.setCurrentIndex(2)
QtCore.QObject.connect(self.clear_button,QtCore.SIGNAL("clicked()"),self.search.clear)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
@ -198,5 +222,5 @@ class Ui_MainWindow(object):
self.action_edit.setShortcut(QtGui.QApplication.translate("MainWindow", "E", None, QtGui.QApplication.UnicodeUTF8))
from widgets import LocationView
from library import BooksView, SearchBox
from library import BooksView, DeviceBooksView, SearchBox
import images_rc

View File

@ -56,7 +56,7 @@ class MovieButton(QLabel):
self.movie = movie
self.setMovie(movie)
self.movie.start()
self.movie.stop()
self.movie.setPaused(True)
class StatusBar(QStatusBar):
def __init__(self):
@ -66,6 +66,16 @@ class StatusBar(QStatusBar):
self.book_info = BookInfoDisplay()
self.addWidget(self.book_info)
def job_added(self, id):
if self.movie_button.movie.state() == QMovie.Paused:
self.movie_button.movie.setPaused(False)
def no_more_jobs(self):
if self.movie_button.movie.state() == QMovie.Running:
self.movie_button.movie.setPaused(True)
self.movie_button.movie.jumpToFrame(0) # This causes MNG error 11, but seems to work regardless
if __name__ == '__main__':
# Used to create the animated status icon
from PyQt4.Qt import QApplication, QPainter, QSvgRenderer, QPixmap, QColor
@ -99,7 +109,6 @@ if __name__ == '__main__':
pixmaps[i].save(name, 'PNG')
filesc = ' '.join(filesl)
cmd = 'convert -dispose Background -delay '+str(delay)+ ' ' + filesc + ' -loop 0 animated.mng'
print cmd
try:
check_call(cmd, shell=True)
finally:

View File

@ -16,7 +16,7 @@
Miscellanous widgets used in the GUI
'''
from PyQt4.QtGui import QListView, QIcon, QFont
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, QSize, SIGNAL
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, QSize, SIGNAL, QObject
from libprs500.gui2 import human_readable, NONE
@ -60,7 +60,6 @@ class LocationModel(QAbstractListModel):
self.free[1] = max(fs[1:])
if cp == None:
self.free[1] = -1
print self.free, self.rowCount(None)
self.reset()
class LocationView(QListView):
@ -69,4 +68,12 @@ class LocationView(QListView):
QListView.__init__(self, parent)
self.setModel(LocationModel(self))
self.reset()
QObject.connect(self.selectionModel(), SIGNAL('currentChanged(QModelIndex, QModelIndex)'), self.current_changed)
def current_changed(self, current, previous):
i = current.row()
location = 'library' if i == 0 else 'main' if i == 1 else 'card'
self.emit(SIGNAL('location_selected(PyQt_PyObject)'), location)

View File

@ -595,14 +595,13 @@ class LibraryDatabase(object):
'''
Filter data based on filters. All the filters must match for an item to
be accepted. Matching is case independent regexp matching.
@param filters: A list of strings suitable for compilation into regexps
@param filters: A list of compiled regexps
@param refilter: If True filters are applied to the results of the previous
filtering.
'''
if not filters:
self.data = self.data if refilter else self.cache
else:
filters = [re.compile(i, re.IGNORECASE) for i in filters if i]
matches = []
for item in self.data if refilter else self.cache:
keep = True