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.
|
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()
|
||||||
|
|
||||||
|
@ -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":"", \
|
||||||
|
@ -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):
|
||||||
|
@ -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>"
|
||||||
@ -33,11 +37,13 @@ class PRS500_PROFILE(object):
|
|||||||
dpi = 166
|
dpi = 166
|
||||||
# Number of pixels to subtract from screen_height when calculating height of text area
|
# Number of pixels to subtract from screen_height when calculating height of text area
|
||||||
fudge = 18
|
fudge = 18
|
||||||
font_size = 10 #: Default (in pt)
|
font_size = 10 #: Default (in pt)
|
||||||
parindent = 80 #: Default (in px)
|
parindent = 80 #: Default (in px)
|
||||||
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)
|
||||||
parindent=int(profile.parindent),
|
tsd = dict(fontsize=fontsize,
|
||||||
linespace=int(10*profile.line_space),
|
parindent=int(profile.parindent),
|
||||||
baselineskip=baselineskip), \
|
linespace=int(10*profile.line_space),
|
||||||
pagestyledefault=ps, blockstyledefault=dict(blockwidth=ps['textwidth']),
|
baselineskip=baselineskip)
|
||||||
**settings)
|
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():
|
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)
|
@ -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,
|
||||||
|
@ -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 />
|
||||||
|
@ -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"]
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
@ -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,8 +56,9 @@ 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)
|
||||||
QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
if slot:
|
||||||
slot)
|
QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||||
|
slot)
|
||||||
job.start()
|
job.start()
|
||||||
|
|
||||||
def job_done(self, id, *args, **kwargs):
|
def job_done(self, id, *args, **kwargs):
|
||||||
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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" />
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user