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. otherwise return list of ebooks in main memory of device.
If True and no books on card return empty list. If True and no books on card return empty list.
@return: A list of Books. Each Book object must have the fields: @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() raise NotImplementedError()
@ -133,3 +133,11 @@ class Device(object):
""" """
raise NotImplementedError() 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 import xml.dom.minidom as dom
from base64 import b64decode as decode from base64 import b64decode as decode
from base64 import b64encode as encode from base64 import b64encode as encode
import time import time, re
MIME_MAP = { \ MIME_MAP = { \
"lrf":"application/x-sony-bbeb", \ "lrf":"application/x-sony-bbeb", \
@ -28,6 +28,9 @@ MIME_MAP = { \
"txt":"text/plain" \ "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): class book_metadata_field(object):
""" Represents metadata stored as an attribute """ """ Represents metadata stored as an attribute """
def __init__(self, attr, formatter=None, setter=None): def __init__(self, attr, formatter=None, setter=None):
@ -48,7 +51,7 @@ class book_metadata_field(object):
class Book(object): class Book(object):
""" Provides a view onto the XML element that represents a book """ """ Provides a view onto the XML element that represents a book """
title = book_metadata_field("title") 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") formatter=lambda x: x if x and x.strip() else "Unknown")
mime = book_metadata_field("mime") mime = book_metadata_field("mime")
rpath = book_metadata_field("path") 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"), 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))) 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 @apply
def thumbnail(): def thumbnail():
doc = \ doc = \
@ -186,6 +201,7 @@ class BookList(list):
sourceid = str(self[0].sourceid) if len(self) else "1" sourceid = str(self[0].sourceid) if len(self) else "1"
attrs = { attrs = {
"title" : info["title"], "title" : info["title"],
'titleSorter' : info['title'],
"author" : info["authors"] if info['authors'] else 'Unknown', \ "author" : info["authors"] if info['authors'] else 'Unknown', \
"page":"0", "part":"0", "scale":"0", \ "page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \ "sourceid":sourceid, "id":str(cid), "date":"", \

View File

@ -813,6 +813,15 @@ class PRS500(Device):
for path in paths: for path in paths:
self.del_file(path, end_session=False) self.del_file(path, end_session=False)
fix_ids(booklists[0], booklists[1]) 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) self.upload_book_list(booklists[0], end_session=False)
if len(booklists[1]): if len(booklists[1]):
self.upload_book_list(booklists[1], end_session=False) self.upload_book_list(booklists[1], end_session=False)
@ -858,9 +867,7 @@ class PRS500(Device):
bkl.add_book(info, name, size, ctime) bkl.add_book(info, name, size, ctime)
fix_ids(booklists[0], booklists[1]) fix_ids(booklists[0], booklists[1])
if sync_booklists: if sync_booklists:
self.upload_book_list(booklists[0], end_session=False) self.sync_booklists(booklists, end_session=False)
if len(booklists[1]):
self.upload_book_list(booklists[1], end_session=False)
@safe @safe
def upload_book_list(self, booklist, end_session=True): 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. 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}. The LRF file format is documented at U{http://www.sven.de/librie/Librie/LrfFormat}.
""" """
import sys, os
from optparse import OptionParser, OptionValueError 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 Book as _Book
from libprs500.ebooks.lrf.pylrs.pylrs import TextBlock, Header, PutObj, \ from libprs500.ebooks.lrf.pylrs.pylrs import TextBlock, Header, PutObj, \
Paragraph, TextStyle, BlockStyle Paragraph, TextStyle, BlockStyle
from libprs500.ebooks.lrf.fonts import FONT_FILE_MAP
from libprs500 import __version__ as VERSION from libprs500 import __version__ as VERSION
from libprs500 import iswindows
__docformat__ = "epytext" __docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
@ -38,6 +42,8 @@ class PRS500_PROFILE(object):
line_space = 1.2 #: Default (in pt) line_space = 1.2 #: Default (in pt)
header_font_size = 6 #: In pt header_font_size = 6 #: In pt
header_height = 30 #: In px 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: else:
raise OptionValueError('Profile: '+value+' is not implemented') 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): class ConversionError(Exception):
pass pass
@ -98,6 +118,24 @@ def option_parser(usage):
page.add_option('--bottom-margin', default=0, dest='bottom_margin', type='int', page.add_option('--bottom-margin', default=0, dest='bottom_margin', type='int',
help='''Bottom margin of page. Default is %default px.''') 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 = parser.add_option_group('DEBUG OPTIONS')
debug.add_option('--verbose', dest='verbose', action='store_true', default=False, debug.add_option('--verbose', dest='verbose', action='store_true', default=False,
help='''Be verbose while processing''') help='''Be verbose while processing''')
@ -105,6 +143,42 @@ def option_parser(usage):
help='Convert to LRS', default=False) help='Convert to LRS', default=False)
return parser 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, def Book(options, font_delta=0, header=None,
profile=PRS500_PROFILE, **settings): profile=PRS500_PROFILE, **settings):
ps = {} 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) ps['textheight'] = profile.screen_height - (options.bottom_margin + ps['topmargin'] + ps['headheight'] + profile.fudge)
fontsize = int(10*profile.font_size+font_delta*20) fontsize = int(10*profile.font_size+font_delta*20)
baselineskip = fontsize + 20 baselineskip = fontsize + 20
return _Book(textstyledefault=dict(fontsize=fontsize, fonts = find_custom_fonts(options)
tsd = dict(fontsize=fontsize,
parindent=int(profile.parindent), parindent=int(profile.parindent),
linespace=int(10*profile.line_space), linespace=int(10*profile.line_space),
baselineskip=baselineskip), \ baselineskip=baselineskip)
pagestyledefault=ps, blockstyledefault=dict(blockwidth=ps['textwidth']), 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) **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(): if name in FONT_MAP.keys():
path = get_font_path(name) path = get_font_path(name)
return ImageFont.truetype(path, size, encoding=encoding) 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 return result
@staticmethod @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 Receives a dictionary of html attributes and styles and returns
approximate Xylog equivalents in a new dictionary approximate Xylog equivalents in a new dictionary
""" """
def font_weight(val): def font_weight(val):
ans = None ans = 0
m = re.search("([0-9]+)", val) m = re.search("([0-9]+)", val)
if m: if m:
ans = str(int(m.group(1))) ans = int(m.group(1))
elif val.find("bold") >= 0 or val.find("strong") >= 0: 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 return ans
def font_family(val): def font_family(val):
ans = None ans = 'serif'
if max(val.find("courier"), val.find("mono"), val.find("fixed"), val.find("typewriter"))>=0: 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"), elif max(val.find("arial"), val.find("helvetica"), val.find("verdana"),
val.find("trebuchet"), val.find("sans")) >= 0: val.find("trebuchet"), val.find("sans")) >= 0:
ans = "Swis721 BT Roman" ans = 'sans'
return ans 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): def font_size(val):
ans = None ans = None
unit = Span.unit_convert(val, dpi, 14) unit = Span.unit_convert(val, dpi, 14)
@ -129,37 +146,38 @@ class Span(_Span):
return ans return ans
t = dict() t = dict()
family, weight, style = 'serif', 'normal', 'normal'
for key in d.keys(): for key in d.keys():
val = d[key].lower() val = d[key].lower()
if key == 'font': if key == 'font':
val = val.split() vals = val.split()
val.reverse() for val in vals:
for sval in val: family = font_family(val)
ans = font_family(sval) if family != 'serif':
if ans: break
t['fontfacename'] = ans for val in vals:
else: weight = font_weight(val)
ans = font_size(sval) if weight != 'normal':
if ans: break
t['fontsize'] = ans for val in vals:
else: style = font_style(val)
ans = font_weight(sval) if style != 'normal':
if ans: break
t['fontweight'] = ans for val in vals:
sz = font_size(val)
if sz:
t['fontsize'] = sz
break
elif key in ['font-family', 'font-name']: elif key in ['font-family', 'font-name']:
ans = font_family(val) family = font_family(val)
if ans:
t['fontfacename'] = ans
elif key == "font-size": elif key == "font-size":
ans = font_size(val) ans = font_size(val)
if ans: if ans:
t['fontsize'] = ans t['fontsize'] = ans
elif key == 'font-weight': elif key == 'font-weight':
ans = font_weight(val) weight = font_weight(val)
if ans: elif key == 'font-style':
t['fontweight'] = ans style = font_style(val)
if int(ans) > 140:
t['wordspace'] = '50'
else: else:
report = True report = True
if memory != None: if memory != None:
@ -169,22 +187,32 @@ class Span(_Span):
memory.append(key) memory.append(key)
if report: if report:
print >>sys.stderr, 'Unhandled/malformed CSS key:', key, d[key] 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 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 = ns.string if hasattr(ns, 'string') else ns
src = re.sub(r'\s{2,}', ' ', src) # Remove multiple spaces src = re.sub(r'\s{2,}', ' ', src) # Remove multiple spaces
for pat, repl in Span.rules: for pat, repl in Span.rules:
src = pat.sub(repl, src) src = pat.sub(repl, src)
if not src: if not src:
raise ConversionError('No point in adding an empty string to a Span') raise ConversionError('No point in adding an empty string to a Span')
if 'font-style' in css.keys(): attrs = Span.translate_attrs(css, dpi, fonts, font_delta=font_delta, memory=memory)
fs = css.pop('font-style') family, key = attrs['fontfacename']
if fs.lower() == 'italic': 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) src = Italic(src)
attrs = Span.translate_attrs(css, dpi, font_delta=font_delta, memory=memory)
if 'fontsize' in attrs.keys(): if 'fontsize' in attrs.keys():
attrs['baselineskip'] = int(attrs['fontsize']) + 20 attrs['baselineskip'] = int(attrs['fontsize']) + 20
if attrs['fontfacename'] == fonts['serif']['normal'][1]:
attrs.pop('fontfacename')
_Span.__init__(self, text=src, **attrs) _Span.__init__(self, text=src, **attrs)
@ -214,7 +242,7 @@ class HTMLConverter(object):
processed_files = {} #: Files that have been processed 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, font_delta=0, verbose=False, cover=None,
max_link_levels=sys.maxint, link_level=0, max_link_levels=sys.maxint, link_level=0,
is_root=True, baen=False, chapter_detection=True, is_root=True, baen=False, chapter_detection=True,
@ -231,6 +259,7 @@ class HTMLConverter(object):
@param book: The LRF book @param book: The LRF book
@type book: L{libprs500.lrf.pylrs.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 @param path: path to the HTML file to process
@type path: C{str} @type path: C{str}
@param width: Width of the device on which the LRF file is to be read @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'}, th = {'font-size' : 'large', 'font-weight':'bold'},
big = {'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.profile = profile #: Defines the geometry of the display device
self.chapter_detection = chapter_detection #: Flag to toggle chapter detection self.chapter_detection = chapter_detection #: Flag to toggle chapter detection
self.chapter_regex = chapter_regex #: Regex used to search for chapter titles self.chapter_regex = chapter_regex #: Regex used to search for chapter titles
@ -553,7 +583,8 @@ class HTMLConverter(object):
path = os.path.abspath(path) path = os.path.abspath(path)
if not path in HTMLConverter.processed_files.keys(): if not path in HTMLConverter.processed_files.keys():
try: try:
self.files[path] = HTMLConverter(self.book, path, self.files[path] = HTMLConverter(
self.book, self.fonts, path,
profile=self.profile, profile=self.profile,
font_delta=self.font_delta, verbose=self.verbose, font_delta=self.font_delta, verbose=self.verbose,
link_level=self.link_level+1, link_level=self.link_level+1,
@ -690,7 +721,7 @@ class HTMLConverter(object):
self.process_alignment(css) self.process_alignment(css)
try: try:
self.current_para.append(Span(src, self.sanctify_css(css), self.memory,\ 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: except ConversionError, err:
if self.verbose: if self.verbose:
print >>sys.stderr, err print >>sys.stderr, err
@ -949,7 +980,8 @@ class HTMLConverter(object):
elif tagname == 'pre': elif tagname == 'pre':
self.end_current_para() self.end_current_para()
self.current_block.append_to(self.current_page) 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 = self.book.create_text_style(**self.unindented_style.attrs)
ts.attrs.update(attrs) ts.attrs.update(attrs)
self.current_block = self.book.create_text_block( self.current_block = self.book.create_text_block(
@ -959,7 +991,7 @@ class HTMLConverter(object):
lines = src.split('\n') lines = src.split('\n')
for line in lines: for line in lines:
try: 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() self.current_para.CR()
except ConversionError: except ConversionError:
pass pass
@ -1145,14 +1177,14 @@ def process_file(path, options):
header.append(Bold(options.title)) header.append(Bold(options.title))
header.append(' by ') header.append(' by ')
header.append(Italic(options.author+" ")) 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 \ le = re.compile(options.link_exclude) if options.link_exclude else \
re.compile('$') re.compile('$')
pb = re.compile(options.page_break, re.IGNORECASE) if options.page_break else \ pb = re.compile(options.page_break, re.IGNORECASE) if options.page_break else \
re.compile('$') re.compile('$')
fpb = re.compile(options.force_page_break, re.IGNORECASE) if options.force_page_break else \ fpb = re.compile(options.force_page_break, re.IGNORECASE) if options.force_page_break else \
re.compile('$') re.compile('$')
conv = HTMLConverter(book, path, profile=options.profile, conv = HTMLConverter(book, fonts, path, profile=options.profile,
font_delta=options.font_delta, font_delta=options.font_delta,
cover=cpath, max_link_levels=options.link_levels, cover=cpath, max_link_levels=options.link_levels,
verbose=options.verbose, baen=options.baen, verbose=options.verbose, baen=options.baen,

View File

@ -8,7 +8,7 @@
</head> </head>
<h1>Demo of <span style='font-family:monospace'>html2lrf</span></h1> <h1>Demo of <span style='font-family:monospace'>html2lrf</span></h1>
<p> <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> </p>
<br/> <br/>
<h2><a name='toc'>Table of Contents</a></h2> <h2><a name='toc'>Table of Contents</a></h2>
@ -17,18 +17,21 @@
<li><a href='#tables'>Tables</a></li> <li><a href='#tables'>Tables</a></li>
<li><a href='#text'>Text formatting and ruled lines</a></li> <li><a href='#text'>Text formatting and ruled lines</a></li>
<li><a href='#images'>Inline images</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='#recursive'>Recursive link following</a></li>
<li><a href='demo_ext.html'>The HTML used to create this file</a> <li><a href='demo_ext.html'>The HTML used to create this file</a>
</ul> </ul>
<h2><a name='lists'>Lists</a></h2> <h2><a name='lists'>Lists</a></h2>
<p><h3>Unordered lists</h3> <p></p>
<h3>Unordered lists</h3>
<ul> <ul>
<li>Item 1</li> <li>Item 1</li>
<li>Item 2</li> <li>Item 2</li>
</ul> </ul>
</p> </p>
<p><h3>Ordered lists</h3> <p></p>
<h3>Ordered lists</h3>
<ol> <ol>
<li>Item 1</li> <li>Item 1</li>
<li>Item 2</li> <li>Item 2</li>
@ -36,7 +39,7 @@
</p> </p>
<br/> <br/>
<p> <p>
Note that nested lists are not supported. Note that nested lists are not supported.<br />
</p> </p>
<p class='toc'> <p class='toc'>
<hr /> <hr />
@ -94,7 +97,7 @@
<h2><a name='text'>Text formatting</a></h2> <h2><a name='text'>Text formatting</a></h2>
<p> <p>
A simple <i>paragraph</i> of <b>formatted 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> </p>
<hr/> <hr/>
<p> A <p> A
@ -113,7 +116,7 @@
<hr/> <hr/>
<p style='text-indent:30em'>A very indented paragraph</p> <p style='text-indent:30em'>A very indented paragraph</p>
<p style='text-indent:0em'>An unindented 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'> <p class='toc'>
<hr /> <hr />
<a href='#toc'>Table of Contents</a> <a href='#toc'>Table of Contents</a>
@ -128,9 +131,29 @@
<a href='#toc'>Table of Contents</a> <a href='#toc'>Table of Contents</a>
</p> </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> <h2><a name='recursive'>Recursive link following</a></h2>
<p> <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. <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>
<p class='toc'> <p class='toc'>
<hr /> <hr />

View File

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

View File

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

View File

@ -75,8 +75,7 @@ class DeviceJob(QThread):
self.id, self.result, exception, last_traceback) self.id, self.result, exception, last_traceback)
def progress_update(self, val): def progress_update(self, val):
print val self.emit(SIGNAL('status_update(int)'), int(val))
self.emit(SIGNAL('status_update(int)'), int(val), Qt.QueuedConnection)
class DeviceManager(QObject): class DeviceManager(QObject):
@ -85,7 +84,7 @@ class DeviceManager(QObject):
self.device_class = device_class self.device_class = device_class
self.device = 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''' ''' Return callable that returns device information and free space on device'''
def get_device_information(updater): def get_device_information(updater):
self.device.set_progress_reporter(updater) self.device.set_progress_reporter(updater)
@ -102,5 +101,12 @@ class DeviceManager(QObject):
self.device.set_progress_reporter(updater) self.device.set_progress_reporter(updater)
mainlist = self.device.books(oncard=False, end_session=False) mainlist = self.device.books(oncard=False, end_session=False)
cardlist = self.device.books(oncard=True) cardlist = self.device.books(oncard=True)
return mainlist, cardlist return (mainlist, cardlist)
return books 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) job = job_class(self.next_id, lock, *args, **kwargs)
QObject.connect(job, SIGNAL('finished()'), self.cleanup_jobs) QObject.connect(job, SIGNAL('finished()'), self.cleanup_jobs)
self.jobs[self.next_id] = job self.jobs[self.next_id] = job
self.emit(SIGNAL('job_added(int)'), self.next_id)
return job return job
finally: finally:
self.job_create_lock.unlock() self.job_create_lock.unlock()
@ -55,6 +56,7 @@ class JobManager(QAbstractTableModel):
job = self.create_job(DeviceJob, self.device_lock, callable, *args, **kwargs) job = self.create_job(DeviceJob, self.device_lock, callable, *args, **kwargs)
QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.job_done) self.job_done)
if slot:
QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
slot) slot)
job.start() job.start()
@ -69,6 +71,8 @@ class JobManager(QAbstractTableModel):
self.cleanup_lock.lock() self.cleanup_lock.lock()
self.cleanup[id] = job self.cleanup[id] = job
self.cleanup_lock.unlock() self.cleanup_lock.unlock()
if len(self.jobs.keys()) == 0:
self.emit(SIGNAL('no_more_jobs()'))
finally: finally:
self.job_remove_lock.unlock() self.job_remove_lock.unlock()

View File

@ -13,7 +13,8 @@
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import os, textwrap, traceback, time, re 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 math import cos, sin, pi
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \ from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
@ -77,66 +78,6 @@ class LibraryDelegate(QItemDelegate):
traceback.print_exc(e) traceback.print_exc(e)
painter.restore() 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): class BooksModel(QAbstractTableModel):
def __init__(self, parent): def __init__(self, parent):
@ -150,7 +91,7 @@ class BooksModel(QAbstractTableModel):
db = LibraryDatabase(os.path.expanduser(str(db))) db = LibraryDatabase(os.path.expanduser(str(db)))
self.db = db self.db = db
def search(self, text, refinement): def search_tokens(self, text):
tokens = [] tokens = []
quot = re.search('"(.*?)"', text) quot = re.search('"(.*?)"', text)
while quot: while quot:
@ -158,6 +99,10 @@ class BooksModel(QAbstractTableModel):
text = text.replace('"'+quot.group(1)+'"', '') text = text.replace('"'+quot.group(1)+'"', '')
quot = re.search('"(.*?)"', text) quot = re.search('"(.*?)"', text)
tokens += text.split(' ') 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.db.filter(tokens, refinement)
self.reset() self.reset()
self.emit(SIGNAL('searched()')) self.emit(SIGNAL('searched()'))
@ -204,7 +149,7 @@ class BooksModel(QAbstractTableModel):
dt = self.db.timestamp(row) dt = self.db.timestamp(row)
if dt: if dt:
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) 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: elif col == 4:
r = self.db.rating(row) r = self.db.rating(row)
r = r/2 if r else 0 r = r/2 if r else 0
@ -218,11 +163,7 @@ class BooksModel(QAbstractTableModel):
return QVariant(Qt.AlignRight | Qt.AlignVCenter) return QVariant(Qt.AlignRight | Qt.AlignVCenter)
elif role == Qt.ToolTipRole and index.isValid(): elif role == Qt.ToolTipRole and index.isValid():
if index.column() in [0, 1, 4, 5]: if index.column() in [0, 1, 4, 5]:
edit = "Double click to <b>edit</b> me<br><br>" return QVariant("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 return NONE
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
@ -232,13 +173,13 @@ class BooksModel(QAbstractTableModel):
if orientation == Qt.Horizontal: if orientation == Qt.Horizontal:
if section == 0: text = "Title" if section == 0: text = "Title"
elif section == 1: text = "Author(s)" elif section == 1: text = "Author(s)"
elif section == 2: text = "Size" elif section == 2: text = "Size (MB)"
elif section == 3: text = "Date" elif section == 3: text = "Date"
elif section == 4: text = "Rating" elif section == 4: text = "Rating"
elif section == 5: text = "Publisher" elif section == 5: text = "Publisher"
return QVariant(self.trUtf8(text)) return QVariant(self.trUtf8(text))
else: else:
return NONE return QVariant(section+1)
def flags(self, index): def flags(self, index):
flags = QAbstractTableModel.flags(self, index) flags = QAbstractTableModel.flags(self, index)
@ -267,10 +208,201 @@ class BooksModel(QAbstractTableModel):
done = True done = True
return done 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): class SearchBox(QLineEdit):
def __init__(self, parent): def __init__(self, parent):
QLineEdit.__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) self.home(False)
QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot) QObject.connect(self, SIGNAL('textEdited(QString)'), self.text_edited_slot)
self.default_palette = QApplication.palette(self) self.default_palette = QApplication.palette(self)
@ -282,13 +414,22 @@ class SearchBox(QLineEdit):
self.timer = None self.timer = None
self.interval = 1000 #: Time to wait before emitting search signal 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): def keyPressEvent(self, event):
if self.initial_state: if self.initial_state:
self.setText('') self.normalize_state()
self.initial_state = False self.initial_state = False
self.setPalette(self.default_palette)
QLineEdit.keyPressEvent(self, event) 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): def text_edited_slot(self, text):
text = str(text) text = str(text)
self.prev_text = text self.prev_text = text

View File

@ -15,11 +15,8 @@
import os, tempfile, sys import os, tempfile, sys
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QSettings, QVariant, QSize, QEventLoop, QString, \ QSettings, QVariant, QSize, QThread
QBuffer, QIODevice, QModelIndex, QThread from PyQt4.QtGui import QErrorMessage
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 import __version__ as VERSION
from libprs500.gui2 import APP_TITLE, installErrorHandler from libprs500.gui2 import APP_TITLE, installErrorHandler
@ -40,6 +37,10 @@ class Main(QObject, Ui_MainWindow):
self.device_manager = None self.device_manager = None
self.temporary_slots = {} self.temporary_slots = {}
####################### Location View ########################
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
self.location_selected)
####################### Vanity ######################## ####################### Vanity ########################
self.vanity_template = self.vanity.text().arg(VERSION) self.vanity_template = self.vanity.text().arg(VERSION)
self.vanity.setText(self.vanity_template.arg(' ')) self.vanity.setText(self.vanity_template.arg(' '))
@ -47,10 +48,17 @@ class Main(QObject, Ui_MainWindow):
####################### Status Bar ##################### ####################### Status Bar #####################
self.status_bar = StatusBar() self.status_bar = StatusBar()
self.window.setStatusBar(self.status_bar) 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.set_database(self.database_path)
self.library_view.connect_to_search_box(self.search) 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.closeEvent = self.close_event
window.show() window.show()
@ -67,16 +75,28 @@ class Main(QObject, Ui_MainWindow):
self.detector.start(QThread.InheritPriority) 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): def job_exception(self, id, exception, formatted_traceback):
raise JobException, str(exception) + '\n\r' + formatted_traceback raise JobException, str(exception) + '\n\r' + formatted_traceback
def device_detected(self, cls, connected): def device_detected(self, cls, connected):
if connected: if connected:
self.device_manager = DeviceManager(cls) 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) self.job_manager.run_device_job(self.info_read, func)
def info_read(self, id, result, exception, formatted_traceback): def info_read(self, id, result, exception, formatted_traceback):
@ -86,6 +106,20 @@ class Main(QObject, Ui_MainWindow):
info, cp, fs = result info, cp, fs = result
self.location_view.model().update_devices(cp, fs) self.location_view.model().update_devices(cp, fs)
self.vanity.setText(self.vanity_template.arg('Connected '+' '.join(info[:-1]))) 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): def read_settings(self):

View File

@ -170,7 +170,7 @@
</layout> </layout>
</item> </item>
<item row="2" column="0" > <item row="2" column="0" >
<widget class="QStackedWidget" name="stacks" > <widget class="QStackedWidget" name="stack" >
<property name="sizePolicy" > <property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" > <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch> <horstretch>100</horstretch>
@ -178,7 +178,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="currentIndex" > <property name="currentIndex" >
<number>0</number> <number>2</number>
</property> </property>
<widget class="QWidget" name="library" > <widget class="QWidget" name="library" >
<layout class="QVBoxLayout" > <layout class="QVBoxLayout" >
@ -218,7 +218,42 @@
<widget class="QWidget" name="main_memory" > <widget class="QWidget" name="main_memory" >
<layout class="QGridLayout" > <layout class="QGridLayout" >
<item row="0" column="0" > <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" > <property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" > <sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch> <horstretch>100</horstretch>
@ -347,6 +382,11 @@
<extends>QListView</extends> <extends>QListView</extends>
<header>widgets.h</header> <header>widgets.h</header>
</customwidget> </customwidget>
<customwidget>
<class>DeviceBooksView</class>
<extends>QTableView</extends>
<header>library.h</header>
</customwidget>
</customwidgets> </customwidgets>
<resources> <resources>
<include location="images.qrc" /> <include location="images.qrc" />

View File

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

View File

@ -56,7 +56,7 @@ class MovieButton(QLabel):
self.movie = movie self.movie = movie
self.setMovie(movie) self.setMovie(movie)
self.movie.start() self.movie.start()
self.movie.stop() self.movie.setPaused(True)
class StatusBar(QStatusBar): class StatusBar(QStatusBar):
def __init__(self): def __init__(self):
@ -66,6 +66,16 @@ class StatusBar(QStatusBar):
self.book_info = BookInfoDisplay() self.book_info = BookInfoDisplay()
self.addWidget(self.book_info) 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__': if __name__ == '__main__':
# Used to create the animated status icon # Used to create the animated status icon
from PyQt4.Qt import QApplication, QPainter, QSvgRenderer, QPixmap, QColor from PyQt4.Qt import QApplication, QPainter, QSvgRenderer, QPixmap, QColor
@ -99,7 +109,6 @@ if __name__ == '__main__':
pixmaps[i].save(name, 'PNG') pixmaps[i].save(name, 'PNG')
filesc = ' '.join(filesl) filesc = ' '.join(filesl)
cmd = 'convert -dispose Background -delay '+str(delay)+ ' ' + filesc + ' -loop 0 animated.mng' cmd = 'convert -dispose Background -delay '+str(delay)+ ' ' + filesc + ' -loop 0 animated.mng'
print cmd
try: try:
check_call(cmd, shell=True) check_call(cmd, shell=True)
finally: finally:

View File

@ -16,7 +16,7 @@
Miscellanous widgets used in the GUI Miscellanous widgets used in the GUI
''' '''
from PyQt4.QtGui import QListView, QIcon, QFont 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 from libprs500.gui2 import human_readable, NONE
@ -60,7 +60,6 @@ class LocationModel(QAbstractListModel):
self.free[1] = max(fs[1:]) self.free[1] = max(fs[1:])
if cp == None: if cp == None:
self.free[1] = -1 self.free[1] = -1
print self.free, self.rowCount(None)
self.reset() self.reset()
class LocationView(QListView): class LocationView(QListView):
@ -69,4 +68,12 @@ class LocationView(QListView):
QListView.__init__(self, parent) QListView.__init__(self, parent)
self.setModel(LocationModel(self)) self.setModel(LocationModel(self))
self.reset() 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 Filter data based on filters. All the filters must match for an item to
be accepted. Matching is case independent regexp matching. 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 @param refilter: If True filters are applied to the results of the previous
filtering. filtering.
''' '''
if not filters: if not filters:
self.data = self.data if refilter else self.cache self.data = self.data if refilter else self.cache
else: else:
filters = [re.compile(i, re.IGNORECASE) for i in filters if i]
matches = [] matches = []
for item in self.data if refilter else self.cache: for item in self.data if refilter else self.cache:
keep = True keep = True