mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Support for embedded fonts in html2lrf. Fix title sorting of books on device. Lots of progress in gui2.
This commit is contained in:
parent
fe2e72d699
commit
4f38f13271
@ -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()
|
||||
|
||||
|
@ -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":"", \
|
||||
|
@ -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):
|
||||
|
@ -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
|
@ -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)
|
@ -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,
|
||||
|
@ -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 />
|
||||
|
@ -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"]
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user