Merge from trunk

This commit is contained in:
Charles Haley 2011-04-25 11:10:38 +01:00
commit 45c5a31cfe
34 changed files with 1729 additions and 745 deletions

View File

@ -53,6 +53,7 @@ class FinancialTimes(BasicNewsRecipe):
feeds = [ feeds = [
(u'UK' , u'http://www.ft.com/rss/home/uk' ) (u'UK' , u'http://www.ft.com/rss/home/uk' )
,(u'US' , u'http://www.ft.com/rss/home/us' ) ,(u'US' , u'http://www.ft.com/rss/home/us' )
,(u'Europe' , u'http://www.ft.com/rss/home/europe' )
,(u'Asia' , u'http://www.ft.com/rss/home/asia' ) ,(u'Asia' , u'http://www.ft.com/rss/home/asia' )
,(u'Middle East', u'http://www.ft.com/rss/home/middleeast') ,(u'Middle East', u'http://www.ft.com/rss/home/middleeast')
] ]

View File

@ -266,26 +266,6 @@ max_content_server_tags_shown=5
content_server_will_display = ['*'] content_server_will_display = ['*']
content_server_wont_display = [] content_server_wont_display = []
#: Set custom metadata fields that the book details panel will or will not display.
# book_details_will_display is a list of custom fields to be displayed.
# book_details_wont_display is a list of custom fields not to be displayed.
# wont_display has priority over will_display.
# The special value '*' means all custom fields. The value [] means no entries.
# Defaults:
# book_details_will_display = ['*']
# book_details_wont_display = []
# Examples:
# To display only the custom fields #mytags and #genre:
# book_details_will_display = ['#mytags', '#genre']
# book_details_wont_display = []
# To display all fields except #mycomments:
# book_details_will_display = ['*']
# book_details_wont_display['#mycomments']
# As above, this tweak affects only display of custom fields. The standard
# fields are not affected
book_details_will_display = ['*']
book_details_wont_display = []
#: Set the maximum number of sort 'levels' #: Set the maximum number of sort 'levels'
# Set the maximum number of sort 'levels' that calibre will use to resort the # Set the maximum number of sort 'levels' that calibre will use to resort the
# library after certain operations such as searches or device insertion. Each # library after certain operations such as searches or device insertion. Each

Binary file not shown.

View File

@ -0,0 +1,41 @@
a {
text-decoration: none;
color: blue
}
.comments {
margin-top: 0;
padding-top: 0;
text-indent: 0
}
table.fields {
margin-bottom: 0;
padding-bottom: 0;
}
table.fields td {
vertical-align: top
}
table.fields td.title {
font-weight: bold
}
.series_name {
font-style: italic
}
/*
The HTML that this styleshhet applies to looks like this:
<table class="fields">
<tr id="formats" class="datatype_text"><td class="title">Formats:</td><td><a href="format:572:EPUB">EPUB</a>, <a href="format:572:LIT">LIT</a></td></tr>
<tr id="series" class="datatype_series"><td class="title">Series:</td><td>Book II of <span class="series_name">The Sea Beggars</span></td></tr>
<tr id="tags" class="datatype_text"><td class="title">Tags:</td><td>Fantasy, Fiction</td></tr>
<tr id="path" class="datatype_text"><td class="title">Path:</td><td><a href="path:572" title="/home/kovid/test library/Paul Kearney/This Forsaken Earth (572)">Click to open</a></td></tr>
</table>
<div id="comments" class="comments"><h3>From Publishers Weekly</h3><p>At the start of Kearney's rousing sequel to <em>The Mark of Ran</em> (2005), Rol Cortishane, the youthful captain of the privateer <em>Revenant</em>, captures a slaver and frees its chained slaves. Back in the harbor of Ganesh Ka in the land of Umer, Rol encounters an untrustworthy acquaintance he hasn't seen in years, Canker, a former king of thieves, who urges Rol to join in the fight to save Rowen, a darkly beautiful queen, whose throne is at risk in mountainous Bionar. That Rowen is Rol's half-sister for whom he has lusted in the past doesn't make Rol's decision to help an easy one. If as in <em>The Mark of Ran</em> the action is more lively at sea than on land, Kearney's solid storytelling and nautical detail worthy of C.S. Forester or Patrick O'Brian will keep readers turning the pages. <em>(Dec.)</em> <br />Copyright © Reed Business Information, a division of Reed Elsevier Inc. All rights reserved. </p><h3>From</h3><p>The sequel to <em>The Mark of Ran</em> (2005) finds heroic young Rol Cortishane grown to be a much-feared sea captain. Deciding to ignore his mysterious past, he spends his energy on ship and crew. He is still an outlaw, however, and the only port he can call home is Ganesh Ka, the endangered city of exiles. When word comes from Rowan, his half-sister, asking him to fight on her behalf, he must weigh the safety of Ganesh Ka against Rowan's treachery in the past. Finally persuaded to aid Rowan, he learns more of betrayal and his heritage in the ensuing battles than he had wanted to know. Kearney's characters are much better developed here than they were in <em>The Mark of Ran</em>, and since the book tells a single story, the plot is tighter. Moreover, because almost all the action transpires in the here and now, the sequel can be read without reference to the predecessor. Since it ends hanging on a particularly bloody cliff, expect to see more of Kearney's excellent maritime fantasy. <em>Frieda Murray</em><br /><em>Copyright © American Library Association. All rights reserved</em></p>
</div>
*/

View File

@ -26,6 +26,7 @@ class ANDROID(USBMS):
0xc92 : [0x100], 0xc92 : [0x100],
0xc97 : [0x226], 0xc97 : [0x226],
0xc99 : [0x0100], 0xc99 : [0x0100],
0xca2 : [0x226],
0xca3 : [0x100], 0xca3 : [0x100],
0xca4 : [0x226], 0xca4 : [0x226],
}, },

View File

@ -19,6 +19,9 @@ from calibre.utils.date import isoformat, format_date
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.formatter import TemplateFormatter from calibre.utils.formatter import TemplateFormatter
def human_readable(size, precision=2):
""" Convert a size in bytes into megabytes """
return ('%.'+str(precision)+'f'+ 'MB') % ((size/(1024.*1024.)),)
NULL_VALUES = { NULL_VALUES = {
'user_metadata': {}, 'user_metadata': {},
@ -551,7 +554,8 @@ class Metadata(object):
def format_field_extended(self, key, series_with_index=True): def format_field_extended(self, key, series_with_index=True):
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
''' '''
returns the tuple (field_name, formatted_value) returns the tuple (field_name, formatted_value, original_value,
field_metadata)
''' '''
# Handle custom series index # Handle custom series index
@ -627,6 +631,8 @@ class Metadata(object):
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'rating': elif datatype == 'rating':
res = res/2.0 res = res/2.0
elif key in ('book_size', 'size'):
res = human_readable(res)
return (name, unicode(res), orig_res, fmeta) return (name, unicode(res), orig_res, fmeta)
return (None, None, None, None) return (None, None, None, None)

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
Read meta information from eReader pdb files. Read meta information from pdb files.
''' '''
__license__ = 'GPL v3' __license__ = 'GPL v3'
@ -13,10 +13,12 @@ import re
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.pdb.header import PdbHeaderReader from calibre.ebooks.pdb.header import PdbHeaderReader
from calibre.ebooks.metadata.ereader import get_metadata as get_eReader from calibre.ebooks.metadata.ereader import get_metadata as get_eReader
from calibre.ebooks.metadata.plucker import get_metadata as get_plucker
MREADER = { MREADER = {
'PNPdPPrs' : get_eReader, 'PNPdPPrs' : get_eReader,
'PNRdPPrs' : get_eReader, 'PNRdPPrs' : get_eReader,
'DataPlkr' : get_plucker,
} }
from calibre.ebooks.metadata.ereader import set_metadata as set_eReader from calibre.ebooks.metadata.ereader import set_metadata as set_eReader

View File

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
'''
Read meta information from Plucker pdb files.
'''
__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import struct
from datetime import datetime
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.pdb.header import PdbHeaderReader
from calibre.ebooks.pdb.plucker.reader import SectionHeader, DATATYPE_METADATA, \
MIBNUM_TO_NAME
def get_metadata(stream, extract_cover=True):
'''
Return metadata as a L{MetaInfo} object
'''
mi = MetaInformation(_('Unknown'), [_('Unknown')])
stream.seek(0)
pheader = PdbHeaderReader(stream)
section_data = None
for i in range(1, pheader.num_sections):
raw_data = pheader.section_data(i)
section_header = SectionHeader(raw_data)
if section_header.type == DATATYPE_METADATA:
section_data = raw_data[8:]
break
if not section_data:
return mi
default_encoding = 'latin-1'
record_count, = struct.unpack('>H', section_data[0:2])
adv = 0
title = None
author = None
pubdate = 0
for i in xrange(record_count):
type, = struct.unpack('>H', section_data[2+adv:4+adv])
length, = struct.unpack('>H', section_data[4+adv:6+adv])
# CharSet
if type == 1:
val, = struct.unpack('>H', section_data[6+adv:8+adv])
default_encoding = MIBNUM_TO_NAME.get(val, 'latin-1')
# Author
elif type == 4:
author = section_data[6+adv+(2*length)]
# Title
elif type == 5:
title = section_data[6+adv+(2*length)]
# Publication Date
elif type == 6:
pubdate, = struct.unpack('>I', section_data[6+adv:6+adv+4])
adv += 2*length
if title:
mi.title = title.replace('\0', '').decode(default_encoding, 'replace')
if author:
author = author.replace('\0', '').decode(default_encoding, 'replace')
mi.author = author.split(',')
mi.pubdate = datetime.fromtimestamp(pubdate)
return mi

View File

@ -206,6 +206,7 @@ class OverDrive(Source):
xref_q = '+'.join(title_tokens) xref_q = '+'.join(title_tokens)
#log.error('Initial query is %s'%initial_q) #log.error('Initial query is %s'%initial_q)
#log.error('Cross reference query is %s'%xref_q) #log.error('Cross reference query is %s'%xref_q)
q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q
query = '{"szKeyword":"'+initial_q+'"}' query = '{"szKeyword":"'+initial_q+'"}'
@ -229,34 +230,42 @@ class OverDrive(Source):
if int(m.group('displayrecords')) >= 1: if int(m.group('displayrecords')) >= 1:
results = True results = True
elif int(m.group('totalrecords')) >= 1: elif int(m.group('totalrecords')) >= 1:
if int(m.group('totalrecords')) >= 100:
if xref_q.find('+') != -1:
xref_tokens = xref_q.split('+')
xref_q = xref_tokens[0]
#log.error('xref_q is '+xref_q)
else:
xref_q = ''
xref_q = '' xref_q = ''
q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q
elif int(m.group('totalrecords')) == 0: elif int(m.group('totalrecords')) == 0:
return '' return ''
return self.sort_ovrdrv_results(raw, title, title_tokens, author, author_tokens) return self.sort_ovrdrv_results(raw, log, title, title_tokens, author, author_tokens)
def sort_ovrdrv_results(self, raw, title=None, title_tokens=None, author=None, author_tokens=None, ovrdrv_id=None): def sort_ovrdrv_results(self, raw, log, title=None, title_tokens=None, author=None, author_tokens=None, ovrdrv_id=None):
close_matches = [] close_matches = []
raw = re.sub('.*?\[\[(?P<content>.*?)\]\].*', '[[\g<content>]]', raw) raw = re.sub('.*?\[\[(?P<content>.*?)\]\].*', '[[\g<content>]]', raw)
results = json.loads(raw) results = json.loads(raw)
#print results #log.error('raw results are:'+str(results))
# The search results are either from a keyword search or a multi-format list from a single ID, # The search results are either from a keyword search or a multi-format list from a single ID,
# sort through the results for closest match/format # sort through the results for closest match/format
if results: if results:
for reserveid, od_title, subtitle, edition, series, publisher, format, formatid, creators, \ for reserveid, od_title, subtitle, edition, series, publisher, format, formatid, creators, \
thumbimage, shortdescription, worldcatlink, excerptlink, creatorfile, sorttitle, \ thumbimage, shortdescription, worldcatlink, excerptlink, creatorfile, sorttitle, \
availabletolibrary, availabletoretailer, relevancyrank, unknown1, unknown2, unknown3 in results: availabletolibrary, availabletoretailer, relevancyrank, unknown1, unknown2, unknown3 in results:
#print "this record's title is "+od_title+", subtitle is "+subtitle+", author[s] are "+creators+", series is "+series #log.error("this record's title is "+od_title+", subtitle is "+subtitle+", author[s] are "+creators+", series is "+series)
if ovrdrv_id is not None and int(formatid) in [1, 50, 410, 900]: if ovrdrv_id is not None and int(formatid) in [1, 50, 410, 900]:
#print "overdrive id is not None, searching based on format type priority" #log.error('overdrive id is not None, searching based on format type priority')
return self.format_results(reserveid, od_title, subtitle, series, publisher, return self.format_results(reserveid, od_title, subtitle, series, publisher,
creators, thumbimage, worldcatlink, formatid) creators, thumbimage, worldcatlink, formatid)
else: else:
if creators:
creators = creators.split(', ') creators = creators.split(', ')
# if an exact match in a preferred format occurs # if an exact match in a preferred format occurs
if (author and creators[0] == author[0]) and od_title == title and int(formatid) in [1, 50, 410, 900] and thumbimage: if ((author and creators[0] == author[0]) or (not author and not creators)) and od_title.lower() == title.lower() and int(formatid) in [1, 50, 410, 900] and thumbimage:
return self.format_results(reserveid, od_title, subtitle, series, publisher, return self.format_results(reserveid, od_title, subtitle, series, publisher,
creators, thumbimage, worldcatlink, formatid) creators, thumbimage, worldcatlink, formatid)
else: else:
@ -282,6 +291,10 @@ class OverDrive(Source):
close_matches.insert(0, self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) close_matches.insert(0, self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
else: else:
close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid)) close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
elif close_title_match and close_author_match and int(formatid) in [1, 50, 410, 900]:
close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
if close_matches: if close_matches:
return close_matches[0] return close_matches[0]
else: else:
@ -289,7 +302,7 @@ class OverDrive(Source):
else: else:
return '' return ''
def overdrive_get_record(self, br, q, ovrdrv_id): def overdrive_get_record(self, br, log, q, ovrdrv_id):
search_url = q+'SearchResults.aspx?ReserveID={'+ovrdrv_id+'}' search_url = q+'SearchResults.aspx?ReserveID={'+ovrdrv_id+'}'
results_url = q+'SearchResults.svc/GetResults?sEcho=1&iColumns=18&sColumns=ReserveID%2CTitle%2CSubtitle%2CEdition%2CSeries%2CPublisher%2CFormat%2CFormatID%2CCreators%2CThumbImage%2CShortDescription%2CWorldCatLink%2CExcerptLink%2CCreatorFile%2CSortTitle%2CAvailableToLibrary%2CAvailableToRetailer%2CRelevancyRank&iDisplayStart=0&iDisplayLength=10&sSearch=&bEscapeRegex=true&iSortingCols=1&iSortCol_0=17&sSortDir_0=asc' results_url = q+'SearchResults.svc/GetResults?sEcho=1&iColumns=18&sColumns=ReserveID%2CTitle%2CSubtitle%2CEdition%2CSeries%2CPublisher%2CFormat%2CFormatID%2CCreators%2CThumbImage%2CShortDescription%2CWorldCatLink%2CExcerptLink%2CCreatorFile%2CSortTitle%2CAvailableToLibrary%2CAvailableToRetailer%2CRelevancyRank&iDisplayStart=0&iDisplayLength=10&sSearch=&bEscapeRegex=true&iSortingCols=1&iSortCol_0=17&sSortDir_0=asc'
@ -311,7 +324,7 @@ class OverDrive(Source):
raw = str(list(raw)) raw = str(list(raw))
clean_cj = mechanize.CookieJar() clean_cj = mechanize.CookieJar()
br.set_cookiejar(clean_cj) br.set_cookiejar(clean_cj)
return self.sort_ovrdrv_results(raw, None, None, None, ovrdrv_id) return self.sort_ovrdrv_results(raw, log, None, None, None, ovrdrv_id)
def find_ovrdrv_data(self, br, log, title, author, isbn, ovrdrv_id=None): def find_ovrdrv_data(self, br, log, title, author, isbn, ovrdrv_id=None):
@ -319,7 +332,7 @@ class OverDrive(Source):
if ovrdrv_id is None: if ovrdrv_id is None:
return self.overdrive_search(br, log, q, title, author) return self.overdrive_search(br, log, q, title, author)
else: else:
return self.overdrive_get_record(br, q, ovrdrv_id) return self.overdrive_get_record(br, log, q, ovrdrv_id)

View File

@ -12,6 +12,7 @@ from calibre.ebooks.pdb.ereader.reader import Reader as ereader_reader
from calibre.ebooks.pdb.palmdoc.reader import Reader as palmdoc_reader from calibre.ebooks.pdb.palmdoc.reader import Reader as palmdoc_reader
from calibre.ebooks.pdb.ztxt.reader import Reader as ztxt_reader from calibre.ebooks.pdb.ztxt.reader import Reader as ztxt_reader
from calibre.ebooks.pdb.pdf.reader import Reader as pdf_reader from calibre.ebooks.pdb.pdf.reader import Reader as pdf_reader
from calibre.ebooks.pdb.plucker.reader import Reader as plucker_reader
FORMAT_READERS = { FORMAT_READERS = {
'PNPdPPrs': ereader_reader, 'PNPdPPrs': ereader_reader,
@ -19,6 +20,7 @@ FORMAT_READERS = {
'zTXTGPlm': ztxt_reader, 'zTXTGPlm': ztxt_reader,
'TEXtREAd': palmdoc_reader, 'TEXtREAd': palmdoc_reader,
'.pdfADBE': pdf_reader, '.pdfADBE': pdf_reader,
'DataPlkr': plucker_reader,
} }
from calibre.ebooks.pdb.palmdoc.writer import Writer as palmdoc_writer from calibre.ebooks.pdb.palmdoc.writer import Writer as palmdoc_writer
@ -37,6 +39,7 @@ IDENTITY_TO_NAME = {
'zTXTGPlm': 'zTXT', 'zTXTGPlm': 'zTXT',
'TEXtREAd': 'PalmDOC', 'TEXtREAd': 'PalmDOC',
'.pdfADBE': 'Adobe Reader', '.pdfADBE': 'Adobe Reader',
'DataPlkr': 'Plucker',
'BVokBDIC': 'BDicty', 'BVokBDIC': 'BDicty',
'DB99DBOS': 'DB (Database program)', 'DB99DBOS': 'DB (Database program)',
@ -50,7 +53,6 @@ IDENTITY_TO_NAME = {
'DATALSdb': 'LIST', 'DATALSdb': 'LIST',
'Mdb1Mdb1': 'MobileDB', 'Mdb1Mdb1': 'MobileDB',
'BOOKMOBI': 'MobiPocket', 'BOOKMOBI': 'MobiPocket',
'DataPlkr': 'Plucker',
'DataSprd': 'QuickSheet', 'DataSprd': 'QuickSheet',
'SM01SMem': 'SuperMemo', 'SM01SMem': 'SuperMemo',
'TEXtTlDc': 'TealDoc', 'TEXtTlDc': 'TealDoc',

View File

@ -129,14 +129,22 @@ class Reader132(FormatReader):
footnoteids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.footnote_offset).decode('cp1252' if self.encoding is None else self.encoding)) footnoteids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.footnote_offset).decode('cp1252' if self.encoding is None else self.encoding))
for fid, i in enumerate(range(self.header_record.footnote_offset + 1, self.header_record.footnote_offset + self.header_record.footnote_count)): for fid, i in enumerate(range(self.header_record.footnote_offset + 1, self.header_record.footnote_offset + self.header_record.footnote_count)):
self.log.debug('Extracting footnote page %i' % i) self.log.debug('Extracting footnote page %i' % i)
html += footnote_to_html(footnoteids[fid], self.decompress_text(i)) if fid < len(footnoteids):
fid = footnoteids[fid]
else:
fid = ''
html += footnote_to_html(fid, self.decompress_text(i))
if self.header_record.sidebar_count > 0: if self.header_record.sidebar_count > 0:
html += '<br /><h1>%s</h1>' % _('Sidebar') html += '<br /><h1>%s</h1>' % _('Sidebar')
sidebarids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.sidebar_offset).decode('cp1252' if self.encoding is None else self.encoding)) sidebarids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.sidebar_offset).decode('cp1252' if self.encoding is None else self.encoding))
for sid, i in enumerate(range(self.header_record.sidebar_offset + 1, self.header_record.sidebar_offset + self.header_record.sidebar_count)): for sid, i in enumerate(range(self.header_record.sidebar_offset + 1, self.header_record.sidebar_offset + self.header_record.sidebar_count)):
self.log.debug('Extracting sidebar page %i' % i) self.log.debug('Extracting sidebar page %i' % i)
html += sidebar_to_html(sidebarids[sid], self.decompress_text(i)) if sid < len(sidebarids):
sid = sidebarids[sid]
else:
sid = ''
html += sidebar_to_html(sid, self.decompress_text(i))
html += '</body></html>' html += '</body></html>'

View File

@ -0,0 +1,738 @@
# -*- coding: utf-8 -*-
#from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL v3'
__copyright__ = '20011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import os
import struct
import zlib
from collections import OrderedDict
from calibre import CurrentDir
from calibre.ebooks.pdb.formatreader import FormatReader
from calibre.ptempfile import TemporaryFile
from calibre.utils.magick import Image, create_canvas
DATATYPE_PHTML = 0
DATATYPE_PHTML_COMPRESSED = 1
DATATYPE_TBMP = 2
DATATYPE_TBMP_COMPRESSED = 3
DATATYPE_MAILTO = 4
DATATYPE_LINK_INDEX = 5
DATATYPE_LINKS = 6
DATATYPE_LINKS_COMPRESSED = 7
DATATYPE_BOOKMARKS = 8
DATATYPE_CATEGORY = 9
DATATYPE_METADATA = 10
DATATYPE_STYLE_SHEET = 11
DATATYPE_FONT_PAGE = 12
DATATYPE_TABLE = 13
DATATYPE_TABLE_COMPRESSED = 14
DATATYPE_COMPOSITE_IMAGE = 15
DATATYPE_PAGELIST_METADATA = 16
DATATYPE_SORTED_URL_INDEX = 17
DATATYPE_SORTED_URL = 18
DATATYPE_SORTED_URL_COMPRESSED = 19
DATATYPE_EXT_ANCHOR_INDEX = 20
DATATYPE_EXT_ANCHOR = 21
DATATYPE_EXT_ANCHOR_COMPRESSED = 22
# IETF IANA MIBenum value for the character set.
# See the http://www.iana.org/assignments/character-sets for valid values.
# Not all character sets are handled by Python. This is a small subset that
# the MIBenum maps to Python standard encodings
# from http://docs.python.org/library/codecs.html#standard-encodings
MIBNUM_TO_NAME = {
3: 'ascii',
4: 'latin_1',
5: 'iso8859_2',
6: 'iso8859_3',
7: 'iso8859_4',
8: 'iso8859_5',
9: 'iso8859_6',
10: 'iso8859_7',
11: 'iso8859_8',
12: 'iso8859_9',
13: 'iso8859_10',
17: 'shift_jis',
18: 'euc_jp',
27: 'utf_7',
36: 'euc_kr',
37: 'iso2022_kr',
38: 'euc_kr',
39: 'iso2022_jp',
40: 'iso2022_jp_2',
106: 'utf-8',
109: 'iso8859_13',
110: 'iso8859_14',
111: 'iso8859_15',
112: 'iso8859_16',
1013: 'utf_16_be',
1014: 'utf_16_le',
1015: 'utf_16',
2009: 'cp850',
2010: 'cp852',
2011: 'cp437',
2013: 'cp862',
2025: 'gb2312',
2026: 'big5',
2028: 'cp037',
2043: 'cp424',
2044: 'cp500',
2046: 'cp855',
2047: 'cp857',
2048: 'cp860',
2049: 'cp861',
2050: 'cp863',
2051: 'cp864',
2052: 'cp865',
2054: 'cp869',
2063: 'cp1026',
2085: 'hz',
2086: 'cp866',
2087: 'cp775',
2089: 'cp858',
2091: 'cp1140',
2102: 'big5hkscs',
2250: 'cp1250',
2251: 'cp1251',
2252: 'cp1252',
2253: 'cp1253',
2254: 'cp1254',
2255: 'cp1255',
2256: 'cp1256',
2257: 'cp1257',
2258: 'cp1258',
}
class HeaderRecord(object):
'''
Plucker header. PDB record 0.
'''
def __init__(self, raw):
self.uid, = struct.unpack('>H', raw[0:2])
# This is labled version in the spec.
# 2 is ZLIB compressed,
# 1 is DOC compressed
self.compression, = struct.unpack('>H', raw[2:4])
self.records, = struct.unpack('>H', raw[4:6])
# uid of the first html file. This should link
# to other files which in turn may link to others.
self.home_html = None
self.reserved = {}
for i in xrange(self.records):
adv = 4*i
name, = struct.unpack('>H', raw[6+adv:8+adv])
id, = struct.unpack('>H', raw[8+adv:10+adv])
self.reserved[id] = name
if name == 0:
self.home_html = id
class SectionHeader(object):
'''
Every sections (record) has this header. It gives
details about the section such as it's uid.
'''
def __init__(self, raw):
self.uid, = struct.unpack('>H', raw[0:2])
self.paragraphs, = struct.unpack('>H', raw[2:4])
self.size, = struct.unpack('>H', raw[4:6])
self.type, = struct.unpack('>B', raw[6])
self.flags, = struct.unpack('>B', raw[7])
class SectionHeaderText(object):
'''
Sub header for text records.
'''
def __init__(self, section_header, raw):
# The uncompressed size of each paragraph.
self.sizes = []
# uncompressed offset of each paragraph starting
# at the beginning of the PHTML.
self.paragraph_offsets = []
# Paragraph attributes.
self.attributes = []
for i in xrange(section_header.paragraphs):
adv = 4*i
self.sizes.append(struct.unpack('>H', raw[adv:2+adv])[0])
self.attributes.append(struct.unpack('>H', raw[2+adv:4+adv])[0])
running_offset = 0
for size in self.sizes:
running_offset += size
self.paragraph_offsets.append(running_offset)
class SectionMetadata(object):
'''
Metadata.
This does not store metadata such as title, or author.
That metadata would be best retrieved with the PDB (plucker)
metdata reader.
This stores document specific information such as the
text encoding.
Note: There is a default encoding but each text section
can be assigned a different encoding.
'''
def __init__(self, raw):
self.default_encoding = 'latin-1'
self.exceptional_uid_encodings = {}
self.owner_id = None
record_count, = struct.unpack('>H', raw[0:2])
adv = 0
for i in xrange(record_count):
type, = struct.unpack('>H', raw[2+adv:4+adv])
length, = struct.unpack('>H', raw[4+adv:6+adv])
# CharSet
if type == 1:
val, = struct.unpack('>H', raw[6+adv:8+adv])
self.default_encoding = MIBNUM_TO_NAME.get(val, 'latin-1')
# ExceptionalCharSets
elif type == 2:
ii_adv = 0
for ii in xrange(length / 2):
uid, = struct.unpack('>H', raw[6+adv+ii_adv:8+adv+ii_adv])
mib, = struct.unpack('>H', raw[8+adv+ii_adv:10+adv+ii_adv])
self.exceptional_uid_encodings[uid] = MIBNUM_TO_NAME.get(mib, 'latin-1')
ii_adv += 4
# OwnerID
elif type == 3:
self.owner_id = struct.unpack('>I', raw[6+adv:10+adv])
# Author, Title, PubDate
# Ignored here. The metadata reader plugin
# will get this info because if it's missing
# the metadata reader plugin will use fall
# back data from elsewhere in the file.
elif type in (4, 5, 6):
pass
# Linked Documents
elif type == 7:
pass
adv += 2*length
class SectionText(object):
'''
Text data. Stores a text section header and the PHTML.
'''
def __init__(self, section_header, raw):
self.header = SectionHeaderText(section_header, raw)
self.data = raw[section_header.paragraphs * 4:]
class SectionCompositeImage(object):
'''
A composite image consists of a a 2D array
of rows and columns. The entries in the array
are uid's.
'''
def __init__(self, raw):
self.columns, = struct.unpack('>H', raw[0:2])
self.rows, = struct.unpack('>H', raw[2:4])
# [
# [uid, uid, uid, ...],
# [uid, uid, uid, ...],
# ...
# ]
#
# Each item in the layout is in it's
# correct position in the final
# composite.
#
# Each item in the layout is a uid
# to an image record.
self.layout = []
offset = 4
for i in xrange(self.rows):
col = []
for j in xrange(self.columns):
col.append(struct.unpack('>H', raw[offset:offset+2])[0])
offset += 2
self.layout.append(col)
class Reader(FormatReader):
'''
Convert a plucker archive into HTML.
TODO:
* UTF 16 and 32 characters.
* Margins.
* Alignment.
* Font color.
* DATATYPE_MAILTO
* DATATYPE_TABLE(_COMPRESSED)
* DATATYPE_EXT_ANCHOR_INDEX
* DATATYPE_EXT_ANCHOR(_COMPRESSED)
'''
def __init__(self, header, stream, log, options):
self.stream = stream
self.log = log
self.options = options
# Mapping of section uid to our internal
# list of sections.
self.uid_section_number = OrderedDict()
self.uid_text_secion_number = OrderedDict()
self.uid_text_secion_encoding = {}
self.uid_image_section_number = {}
self.uid_composite_image_section_number = {}
self.metadata_section_number = None
self.default_encoding = 'latin-1'
self.owner_id = None
self.sections = []
# The Plucker record0 header
self.header_record = HeaderRecord(header.section_data(0))
for i in range(1, header.num_sections):
section_number = len(self.sections)
# The length of the section header.
# Where the actual data in the section starts.
start = 8
section = None
raw_data = header.section_data(i)
# Every sections has a section header.
section_header = SectionHeader(raw_data)
# Store sections we care able.
if section_header.type in (DATATYPE_PHTML, DATATYPE_PHTML_COMPRESSED):
self.uid_text_secion_number[section_header.uid] = section_number
section = SectionText(section_header, raw_data[start:])
elif section_header.type in (DATATYPE_TBMP, DATATYPE_TBMP_COMPRESSED):
self.uid_image_section_number[section_header.uid] = section_number
section = raw_data[start:]
elif section_header.type == DATATYPE_METADATA:
self.metadata_section_number = section_number
section = SectionMetadata(raw_data[start:])
elif section_header.type == DATATYPE_COMPOSITE_IMAGE:
self.uid_composite_image_section_number[section_header.uid] = section_number
section = SectionCompositeImage(raw_data[start:])
# Store the section.
if section:
self.uid_section_number[section_header.uid] = section_number
self.sections.append((section_header, section))
# Store useful information from the metadata section locally
# to make access easier.
if self.metadata_section_number:
mdata_section = self.sections[self.metadata_section_number][1]
for k, v in mdata_section.exceptional_uid_encodings.items():
self.uid_text_secion_encoding[k] = v
self.default_encoding = mdata_section.default_encoding
self.owner_id = mdata_section.owner_id
# Get the metadata (tile, author, ...) with the metadata reader.
from calibre.ebooks.metadata.pdb import get_metadata
self.mi = get_metadata(stream, False)
def extract_content(self, output_dir):
# Each text record is independent (unless the continuation
# value is set in the previous record). Put each converted
# text recored into a separate file. We will reference the
# home.html file as the first file and let the HTML input
# plugin assemble the order based on hyperlinks.
with CurrentDir(output_dir):
for uid, num in self.uid_text_secion_number.items():
self.log.debug(_('Writing record with uid: %s as %s.html' % (uid, uid)))
with open('%s.html' % uid, 'wb') as htmlf:
html = u'<html><body>'
section_header, section_data = self.sections[num]
if section_header.type == DATATYPE_PHTML:
html += self.process_phtml(section_data.data, section_data.header.paragraph_offsets)
elif section_header.type == DATATYPE_PHTML_COMPRESSED:
d = self.decompress_phtml(section_data.data)
html += self.process_phtml(d, section_data.header.paragraph_offsets).decode(self.get_text_uid_encoding(section_header.uid), 'replace')
html += '</body></html>'
htmlf.write(html.encode('utf-8'))
# Images.
# Cache the image sizes in case they are used by a composite image.
image_sizes = {}
if not os.path.exists(os.path.join(output_dir, 'images/')):
os.makedirs(os.path.join(output_dir, 'images/'))
with CurrentDir(os.path.join(output_dir, 'images/')):
# Single images.
for uid, num in self.uid_image_section_number.items():
section_header, section_data = self.sections[num]
if section_data:
idata = None
if section_header.type == DATATYPE_TBMP:
idata = section_data
elif section_header.type == DATATYPE_TBMP_COMPRESSED:
if self.header_record.compression == 1:
idata = decompress_doc(section_data)
elif self.header_record.compression == 2:
idata = zlib.decompress(section_data)
try:
with TemporaryFile(suffix='.palm') as itn:
with open(itn, 'wb') as itf:
itf.write(idata)
im = Image()
im.read(itn)
image_sizes[uid] = im.size
im.set_compression_quality(70)
im.save('%s.jpg' % uid)
self.log.debug('Wrote image with uid %s to images/%s.jpg' % (uid, uid))
except Exception as e:
self.log.error('Failed to write image with uid %s: %s' % (uid, e))
else:
self.log.error('Failed to write image with uid %s: No data.' % uid)
# Composite images.
# We're going to use the already compressed .jpg images here.
for uid, num in self.uid_composite_image_section_number.items():
try:
section_header, section_data = self.sections[num]
# Get the final width and height.
width = 0
height = 0
for row in section_data.layout:
row_width = 0
col_height = 0
for col in row:
if col not in image_sizes:
raise Exception('Image with uid: %s missing.' % col)
im = Image()
im.read('%s.jpg' % col)
w, h = im.size
row_width += w
if col_height < h:
col_height = h
if width < row_width:
width = row_width
height += col_height
# Create a new image the total size of all image
# parts. Put the parts into the new image.
canvas = create_canvas(width, height)
y_off = 0
for row in section_data.layout:
x_off = 0
largest_height = 0
for col in row:
im = Image()
im.read('%s.jpg' % col)
canvas.compose(im, x_off, y_off)
w, h = im.size
x_off += w
if largest_height < h:
largest_height = h
y_off += largest_height
canvas.set_compression_quality(70)
canvas.save('%s.jpg' % uid)
self.log.debug('Wrote composite image with uid %s to images/%s.jpg' % (uid, uid))
except Exception as e:
self.log.error('Failed to write composite image with uid %s: %s' % (uid, e))
# Run the HTML through the html processing plugin.
from calibre.customize.ui import plugin_for_input_format
html_input = plugin_for_input_format('html')
for opt in html_input.options:
setattr(self.options, opt.option.name, opt.recommended_value)
self.options.input_encoding = 'utf-8'
odi = self.options.debug_pipeline
self.options.debug_pipeline = None
# Determine the home.html record uid. This should be set in the
# reserved values in the metadata recored. home.html is the first
# text record (should have hyper link references to other records)
# in the document.
try:
home_html = self.header_record.home_html
if not home_html:
home_html = self.uid_text_secion_number.items()[0][0]
except:
raise Exception(_('Could not determine home.html'))
# Generate oeb from html conversion.
oeb = html_input.convert(open('%s.html' % home_html, 'rb'), self.options, 'html', self.log, {})
self.options.debug_pipeline = odi
return oeb
def decompress_phtml(self, data):
if self.header_record.compression == 2:
if self.owner_id:
raise NotImplementedError
return zlib.decompress(data)
elif self.header_record.compression == 1:
from calibre.ebooks.compression.palmdoc import decompress_doc
return decompress_doc(data)
def process_phtml(self, d, paragraph_offsets=[]):
html = u'<p id="p0">'
offset = 0
paragraph_open = True
link_open = False
need_set_p_id = False
p_num = 1
font_specifier_close = ''
while offset < len(d):
if not paragraph_open:
if need_set_p_id:
html += u'<p id="p%s">' % p_num
p_num += 1
need_set_p_id = False
else:
html += u'<p>'
paragraph_open = True
c = ord(d[offset])
# PHTML "functions"
if c == 0x0:
offset += 1
c = ord(d[offset])
# Page link begins
# 2 Bytes
# record ID
if c == 0x0a:
offset += 1
id = struct.unpack('>H', d[offset:offset+2])[0]
if id in self.uid_text_secion_number:
html += '<a href="%s.html">' % id
link_open = True
offset += 1
# Targeted page link begins
# 3 Bytes
# record ID, target
elif c == 0x0b:
offset += 3
# Paragraph link begins
# 4 Bytes
# record ID, paragraph number
elif c == 0x0c:
offset += 1
id = struct.unpack('>H', d[offset:offset+2])[0]
offset += 2
pid = struct.unpack('>H', d[offset:offset+2])[0]
if id in self.uid_text_secion_number:
html += '<a href="%s.html#p%s">' % (id, pid)
link_open = True
offset += 1
# Targeted paragraph link begins
# 5 Bytes
# record ID, paragraph number, target
elif c == 0x0d:
offset += 5
# Link ends
# 0 Bytes
elif c == 0x08:
if link_open:
html += '</a>'
link_open = False
# Set font
# 1 Bytes
# font specifier
elif c == 0x11:
offset += 1
specifier = d[offset]
html += font_specifier_close
# Regular text
if specifier == 0:
font_specifier_close = ''
# h1
elif specifier == 1:
html += '<h1>'
font_specifier_close = '</h1>'
# h2
elif specifier == 2:
html += '<h2>'
font_specifier_close = '</h2>'
# h3
elif specifier == 3:
html += '<h13>'
font_specifier_close = '</h3>'
# h4
elif specifier == 4:
html += '<h4>'
font_specifier_close = '</h4>'
# h5
elif specifier == 5:
html += '<h5>'
font_specifier_close = '</h5>'
# h6
elif specifier == 6:
html += '<h6>'
font_specifier_close = '</h6>'
# Bold
elif specifier == 7:
html += '<b>'
font_specifier_close = '</b>'
# Fixed-width
elif specifier == 8:
html += '<tt>'
font_specifier_close = '</tt>'
# Small
elif specifier == 9:
html += '<small>'
font_specifier_close = '</small>'
# Subscript
elif specifier == 10:
html += '<sub>'
font_specifier_close = '</sub>'
# Superscript
elif specifier == 11:
html += '<sup>'
font_specifier_close = '</sup>'
# Embedded image
# 2 Bytes
# image record ID
elif c == 0x1a:
offset += 1
uid = struct.unpack('>H', d[offset:offset+2])[0]
html += '<img src="images/%s.jpg" />' % uid
offset += 1
# Set margin
# 2 Bytes
# left margin, right margin
elif c == 0x22:
offset += 2
# Alignment of text
# 1 Bytes
# alignment
elif c == 0x29:
offset += 1
# Horizontal rule
# 3 Bytes
# 8-bit height, 8-bit width (pixels), 8-bit width (%, 1-100)
elif c == 0x33:
offset += 3
if paragraph_open:
html += u'</p>'
paragraph_open = False
html += u'<hr />'
# New line
# 0 Bytes
elif c == 0x38:
if paragraph_open:
html += u'</p>\n'
paragraph_open = False
# Italic text begins
# 0 Bytes
elif c == 0x40:
html += u'<i>'
# Italic text ends
# 0 Bytes
elif c == 0x48:
html += u'</i>'
# Set text color
# 3 Bytes
# 8-bit red, 8-bit green, 8-bit blue
elif c == 0x53:
offset += 3
# Multiple embedded image
# 4 Bytes
# alternate image record ID, image record ID
elif c == 0x5c:
offset += 3
uid = struct.unpack('>H', d[offset:offset+2])[0]
html += '<img src="images/%s.jpg" />' % uid
offset += 1
# Underline text begins
# 0 Bytes
elif c == 0x60:
html += u'<u>'
# Underline text ends
# 0 Bytes
elif c == 0x68:
html += u'</u>'
# Strike-through text begins
# 0 Bytes
elif c == 0x70:
html += u'<s>'
# Strike-through text ends
# 0 Bytes
elif c == 0x78:
html += u'</s>'
# 16-bit Unicode character
# 3 Bytes
# alternate text length, 16-bit unicode character
elif c == 0x83:
offset += 3
# 32-bit Unicode character
# 5 Bytes
# alternate text length, 32-bit unicode character
elif c == 0x85:
offset += 5
# Begin custom font span
# 6 Bytes
# font page record ID, X page position, Y page position
elif c == 0x8e:
offset += 6
# Adjust custom font glyph position
# 4 Bytes
# X page position, Y page position
elif c == 0x8c:
offset += 4
# Change font page
# 2 Bytes
# font record ID
elif c == 0x8a:
offset += 2
# End custom font span
# 0 Bytes
elif c == 0x88:
pass
# Begin new table row
# 0 Bytes
elif c == 0x90:
pass
# Insert table (or table link)
# 2 Bytes
# table record ID
elif c == 0x92:
offset += 2
# Table cell data
# 7 Bytes
# 8-bit alignment, 16-bit image record ID, 8-bit columns, 8-bit rows, 16-bit text length
elif c == 0x97:
offset += 7
# Exact link modifier
# 2 Bytes
# Paragraph Offset (The Exact Link Modifier modifies a Paragraph Link or Targeted Paragraph Link function to specify an exact byte offset within the paragraph. This function must be followed immediately by the function it modifies).
elif c == 0x9a:
offset += 2
elif c == 0xa0:
html += '&nbsp;'
else:
html += unichr(c)
offset += 1
if offset in paragraph_offsets:
need_set_p_id = True
if paragraph_open:
html += u'</p>\n'
paragraph_open = False
if paragraph_open:
html += u'</p>'
return html
def get_text_uid_encoding(self, uid):
# Return the user sepcified input encoding,
# otherwise return the alternate encoding specified for the uid,
# otherwise retur the default encoding for the document.
return self.options.input_encoding if self.options.input_encoding else self.uid_text_secion_encoding.get(uid, self.default_encoding)

View File

@ -749,7 +749,10 @@ def pml_to_html(pml):
def footnote_sidebar_to_html(pre_id, id, pml): def footnote_sidebar_to_html(pre_id, id, pml):
id = id.strip('\x01') id = id.strip('\x01')
html = '<br /><br style="page-break-after: always;" /><div id="%s-%s"><p>%s</p><small><a href="#r%s-%s">return</a></small></div>' % (pre_id, id, pml_to_html(pml), pre_id, id) if id.strip():
html = '<br /><br style="page-break-after: always;" /><div id="%s-%s">%s<small><a href="#r%s-%s">return</a></small></div>' % (pre_id, id, pml_to_html(pml), pre_id, id)
else:
html = '<br /><br style="page-break-after: always;" /><div>%s</div>' % pml_to_html(pml)
return html return html
def footnote_to_html(id, pml): def footnote_to_html(id, pml):

View File

@ -80,6 +80,14 @@ gprefs.defaults['font'] = None
gprefs.defaults['tags_browser_partition_method'] = 'first letter' gprefs.defaults['tags_browser_partition_method'] = 'first letter'
gprefs.defaults['tags_browser_collapse_at'] = 100 gprefs.defaults['tags_browser_collapse_at'] = 100
gprefs.defaults['edit_metadata_single_layout'] = 'default' gprefs.defaults['edit_metadata_single_layout'] = 'default'
gprefs.defaults['book_display_fields'] = [
('title', False), ('authors', False), ('formats', True),
('series', True), ('identifiers', True), ('tags', True),
('path', True), ('publisher', False), ('rating', False),
('author_sort', False), ('sort', False), ('timestamp', False),
('uuid', False), ('comments', True), ('id', False), ('pubdate', False),
('last_modified', False), ('size', False),
]
# }}} # }}}
@ -89,7 +97,7 @@ UNDEFINED_QDATE = QDate(UNDEFINED_DATE)
ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher', ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher',
'tags', 'series', 'pubdate'] 'tags', 'series', 'pubdate']
def _config(): def _config(): # {{{
c = Config('gui', 'preferences for the calibre GUI') c = Config('gui', 'preferences for the calibre GUI')
c.add_opt('send_to_storage_card_by_default', default=False, c.add_opt('send_to_storage_card_by_default', default=False,
help=_('Send file to storage card instead of main memory by default')) help=_('Send file to storage card instead of main memory by default'))
@ -181,6 +189,8 @@ def _config():
return ConfigProxy(c) return ConfigProxy(c)
config = _config() config = _config()
# }}}
# Turn off DeprecationWarnings in windows GUI # Turn off DeprecationWarnings in windows GUI
if iswindows: if iswindows:
import warnings import warnings

View File

@ -529,13 +529,17 @@ class EditMetadataAction(InterfaceAction):
view.reset() view.reset()
# Apply bulk metadata changes {{{ # Apply bulk metadata changes {{{
def apply_metadata_changes(self, id_map, title=None, msg=''): def apply_metadata_changes(self, id_map, title=None, msg='', callback=None):
''' '''
Apply the metadata changes in id_map to the database synchronously Apply the metadata changes in id_map to the database synchronously
id_map must be a mapping of ids to Metadata objects. Set any fields you id_map must be a mapping of ids to Metadata objects. Set any fields you
do not want updated in the Metadata object to null. An easy way to do do not want updated in the Metadata object to null. An easy way to do
that is to create a metadata object as Metadata(_('Unknown')) and then that is to create a metadata object as Metadata(_('Unknown')) and then
only set the fields you want changed on this object. only set the fields you want changed on this object.
callback can be either None or a function accepting a single argument,
in which case it is called after applying is complete with the list of
changed ids.
''' '''
if title is None: if title is None:
title = _('Applying changed metadata') title = _('Applying changed metadata')
@ -544,6 +548,7 @@ class EditMetadataAction(InterfaceAction):
self.apply_failures = [] self.apply_failures = []
self.applied_ids = [] self.applied_ids = []
self.apply_pd = None self.apply_pd = None
self.apply_callback = callback
if len(self.apply_id_map) > 1: if len(self.apply_id_map) > 1:
from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.dialogs.progress import ProgressDialog
self.apply_pd = ProgressDialog(title, msg, min=0, self.apply_pd = ProgressDialog(title, msg, min=0,
@ -611,6 +616,11 @@ class EditMetadataAction(InterfaceAction):
self.apply_id_map = [] self.apply_id_map = []
self.apply_pd = None self.apply_pd = None
try:
if callable(self.apply_callback):
self.apply_callback(self.applied_ids)
finally:
self.apply_callback = None
# }}} # }}}

View File

@ -30,5 +30,5 @@ class ShowBookDetailsAction(InterfaceAction):
index = self.gui.library_view.currentIndex() index = self.gui.library_view.currentIndex()
if index.isValid(): if index.isValid():
BookInfo(self.gui, self.gui.library_view, index, BookInfo(self.gui, self.gui.library_view, index,
self.gui.iactions['View'].view_format_by_id).show() self.gui.book_details.handle_click).show()

View File

@ -5,67 +5,152 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import collections, sys
from Queue import Queue
from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \ from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl,
QPropertyAnimation, QEasingCurve, QThread, QApplication, QFontInfo, \ QPropertyAnimation, QEasingCurve, QApplication, QFontInfo,
QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu)
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
from calibre import fit_image, prepare_string_for_xml from calibre import fit_image, force_unicode, prepare_string_for_xml
from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \ from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files,
IMAGE_EXTENSIONS, dnd_has_extension IMAGE_EXTENSIONS, dnd_has_extension)
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import preferred_encoding from calibre.ebooks.metadata.book.base import (field_metadata, Metadata)
from calibre.ebooks.metadata import fmt_sidx
from calibre.constants import filesystem_encoding
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
from calibre.gui2 import config, open_local_file, open_url, pixmap_to_data from calibre.gui2 import (config, open_local_file, open_url, pixmap_to_data,
gprefs)
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
# render_rows(data) {{{
WEIGHTS = collections.defaultdict(lambda : 100)
WEIGHTS[_('Path')] = 5
WEIGHTS[_('Formats')] = 1
WEIGHTS[_('Collections')] = 2
WEIGHTS[_('Series')] = 3
WEIGHTS[_('Tags')] = 4
def render_rows(data): def render_html(mi, css, vertical, widget, all_fields=False): # {{{
keys = data.keys() table = render_data(mi, all_fields=all_fields,
# First sort by name. The WEIGHTS sort will preserve this sub-order use_roman_numbers=config['use_roman_numerals_for_series_number'])
keys.sort(key=sort_key)
keys.sort(key=lambda x: WEIGHTS[x]) def color_to_string(col):
rows = [] ans = '#000000'
for key in keys: if col.isValid():
txt = data[key] col = col.toRgb()
if key in ('id', _('Comments')) or not hasattr(txt, 'strip') or not txt.strip() or \ if col.isValid():
txt == 'None': ans = unicode(col.name())
continue return ans
if isinstance(key, str):
key = key.decode(preferred_encoding, 'replace') f = QFontInfo(QApplication.font(widget)).pixelSize()
if isinstance(txt, str): c = color_to_string(QApplication.palette().color(QPalette.Normal,
txt = txt.decode(preferred_encoding, 'replace') QPalette.WindowText))
if key.endswith(u':html'): templ = u'''\
key = key[:-5] <html>
txt = comments_to_html(txt) <head>
elif '</font>' not in txt: <style type="text/css">
txt = prepare_string_for_xml(txt) body, td {background-color: transparent; font-size: %dpx; color: %s }
if 'id' in data: </style>
if key == _('Path'): <style type="text/css">
txt = u'<a href="path:%s" title="%s">%s</a>'%(data['id'], %s
txt, _('Click to open')) </style>
if key == _('Formats') and txt and txt != _('None'): </head>
fmts = [x.strip() for x in txt.split(',')] <body>
fmts = [u'<a href="format:%s:%s">%s</a>' % (data['id'], x, x) for x %%s
in fmts] </body>
txt = ', '.join(fmts) <html>
'''%(f, c, css)
comments = u''
if mi.comments:
comments = comments_to_html(force_unicode(mi.comments))
right_pane = u'<div id="comments" class="comments">%s</div>'%comments
if vertical:
ans = templ%(table+right_pane)
else: else:
if key == _('Path'): ans = templ%(u'<table><tr><td valign="top" '
txt = u'<a href="devpath:%s">%s</a>'%(txt, 'style="padding-right:2em; width:40%%">%s</td><td valign="top">%s</td></tr></table>'
_('Click to open')) % (table, right_pane))
return ans
rows.append((key, txt)) def get_field_list(fm, use_defaults=False):
return rows src = gprefs.defaults if use_defaults else gprefs
fieldlist = list(src['book_display_fields'])
names = frozenset([x[0] for x in fieldlist])
for field in fm.displayable_field_keys():
if field not in names:
fieldlist.append((field, True))
return fieldlist
def render_data(mi, use_roman_numbers=True, all_fields=False):
ans = []
isdevice = not hasattr(mi, 'id')
fm = getattr(mi, 'field_metadata', field_metadata)
for field, display in get_field_list(fm):
metadata = fm.get(field, None)
if all_fields:
display = True
if (not display or not metadata or mi.is_null(field) or
field == 'comments'):
continue
name = metadata['name']
if not name:
name = field
name += ':'
if metadata['datatype'] == 'comments':
val = getattr(mi, field)
if val:
val = force_unicode(val)
ans.append((field,
u'<td class="comments" colspan="2">%s</td>'%comments_to_html(val)))
elif field == 'path':
if mi.path:
path = force_unicode(mi.path, filesystem_encoding)
scheme = u'devpath' if isdevice else u'path'
url = prepare_string_for_xml(path if isdevice else
unicode(mi.id), True)
link = u'<a href="%s:%s" title="%s">%s</a>' % (scheme, url,
prepare_string_for_xml(path, True), _('Click to open'))
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, link)))
elif field == 'formats':
if isdevice: continue
fmts = [u'<a href="format:%s:%s">%s</a>' % (mi.id, x, x) for x
in mi.formats]
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name,
u', '.join(fmts))))
elif field == 'identifiers':
pass # TODO
else:
val = mi.format_field(field)[-1]
if val is None:
continue
val = prepare_string_for_xml(val)
if metadata['datatype'] == 'series':
if metadata['is_custom']:
sidx = mi.get_extra(field)
else:
sidx = getattr(mi, field+'_index')
if sidx is None:
sidx = 1.0
val = _('Book %s of <span class="series_name">%s</span>')%(fmt_sidx(sidx,
use_roman=use_roman_numbers),
prepare_string_for_xml(getattr(mi, field)))
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, val)))
dc = getattr(mi, 'device_collections', [])
if dc:
dc = u', '.join(sorted(dc, key=sort_key))
ans.append(('device_collections',
u'<td class="title">%s</td><td>%s</td>'%(
_('Collections')+':', dc)))
def classname(field):
try:
dt = fm[field]['datatype']
except:
dt = 'text'
return 'datatype_%s'%dt
ans = [u'<tr id="%s" class="%s">%s</tr>'%(field.replace('#', '_'),
classname(field), html) for field, html in ans]
# print '\n'.join(ans)
return u'<table class="fields">%s</table>'%(u'\n'.join(ans))
# }}} # }}}
@ -117,10 +202,10 @@ class CoverView(QWidget): # {{{
def show_data(self, data): def show_data(self, data):
self.animation.stop() self.animation.stop()
same_item = data.get('id', True) == self.data.get('id', False) same_item = getattr(data, 'id', True) == self.data.get('id', False)
self.data = {'id':data.get('id', None)} self.data = {'id':data.get('id', None)}
if data.has_key('cover'): if data.cover_data[1]:
self.pixmap = QPixmap.fromImage(data.pop('cover')) self.pixmap = QPixmap.fromImage(data.cover_data[1])
if self.pixmap.isNull() or self.pixmap.width() < 5 or \ if self.pixmap.isNull() or self.pixmap.width() < 5 or \
self.pixmap.height() < 5: self.pixmap.height() < 5:
self.pixmap = self.default_pixmap self.pixmap = self.default_pixmap
@ -188,32 +273,6 @@ class CoverView(QWidget): # {{{
# Book Info {{{ # Book Info {{{
class RenderComments(QThread):
rdone = pyqtSignal(object, object)
def __init__(self, parent):
QThread.__init__(self, parent)
self.queue = Queue()
self.start()
def run(self):
while True:
try:
rows, comments = self.queue.get()
except:
break
import time
time.sleep(0.001)
oint = sys.getcheckinterval()
sys.setcheckinterval(5)
try:
self.rdone.emit(rows, comments_to_html(comments))
except:
pass
sys.setcheckinterval(oint)
class BookInfo(QWebView): class BookInfo(QWebView):
link_clicked = pyqtSignal(object) link_clicked = pyqtSignal(object)
@ -221,8 +280,6 @@ class BookInfo(QWebView):
def __init__(self, vertical, parent=None): def __init__(self, vertical, parent=None):
QWebView.__init__(self, parent) QWebView.__init__(self, parent)
self.vertical = vertical self.vertical = vertical
self.renderer = RenderComments(self)
self.renderer.rdone.connect(self._show_data, type=Qt.QueuedConnection)
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks) self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
self.linkClicked.connect(self.link_activated) self.linkClicked.connect(self.link_activated)
self._link_clicked = False self._link_clicked = False
@ -231,6 +288,7 @@ class BookInfo(QWebView):
self.setAcceptDrops(False) self.setAcceptDrops(False)
palette.setBrush(QPalette.Base, Qt.transparent) palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette) self.page().setPalette(palette)
self.css = P('templates/book_details.css', data=True).decode('utf-8')
def link_activated(self, link): def link_activated(self, link):
self._link_clicked = True self._link_clicked = True
@ -240,56 +298,9 @@ class BookInfo(QWebView):
def turnoff_scrollbar(self, *args): def turnoff_scrollbar(self, *args):
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
def show_data(self, data): def show_data(self, mi):
rows = render_rows(data) html = render_html(mi, self.css, self.vertical, self.parent())
rows = u'\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for self.setHtml(html)
k, t in rows])
comments = data.get(_('Comments'), '')
if not comments or comments == u'None':
comments = ''
self.renderer.queue.put((rows, comments))
self._show_data(rows, '')
def _show_data(self, rows, comments):
def color_to_string(col):
ans = '#000000'
if col.isValid():
col = col.toRgb()
if col.isValid():
ans = unicode(col.name())
return ans
f = QFontInfo(QApplication.font(self.parent())).pixelSize()
c = color_to_string(QApplication.palette().color(QPalette.Normal,
QPalette.WindowText))
templ = u'''\
<html>
<head>
<style type="text/css">
body, td {background-color: transparent; font-size: %dpx; color: %s }
a { text-decoration: none; color: blue }
div.description { margin-top: 0; padding-top: 0; text-indent: 0 }
table { margin-bottom: 0; padding-bottom: 0; }
</style>
</head>
<body>
%%s
</body>
<html>
'''%(f, c)
if self.vertical:
extra = ''
if comments:
extra = u'<div class="description">%s</div>'%comments
self.setHtml(templ%(u'<table>%s</table>%s'%(rows, extra)))
else:
left_pane = u'<table>%s</table>'%rows
right_pane = u'<div>%s</div>'%comments
self.setHtml(templ%(u'<table><tr><td valign="top" '
'style="padding-right:2em; width:40%%">%s</td><td valign="top">%s</td></tr></table>'
% (left_pane, right_pane)))
def mouseDoubleClickEvent(self, ev): def mouseDoubleClickEvent(self, ev):
swidth = self.page().mainFrame().scrollBarGeometry(Qt.Vertical).width() swidth = self.page().mainFrame().scrollBarGeometry(Qt.Vertical).width()
@ -457,10 +468,10 @@ class BookDetails(QWidget): # {{{
self._layout.addWidget(self.cover_view) self._layout.addWidget(self.cover_view)
self.book_info = BookInfo(vertical, self) self.book_info = BookInfo(vertical, self)
self._layout.addWidget(self.book_info) self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self._link_clicked) self.book_info.link_clicked.connect(self.handle_click)
self.setCursor(Qt.PointingHandCursor) self.setCursor(Qt.PointingHandCursor)
def _link_clicked(self, link): def handle_click(self, link):
typ, _, val = link.partition(':') typ, _, val = link.partition(':')
if typ == 'path': if typ == 'path':
self.open_containing_folder.emit(int(val)) self.open_containing_folder.emit(int(val))
@ -484,7 +495,7 @@ class BookDetails(QWidget): # {{{
def show_data(self, data): def show_data(self, data):
self.book_info.show_data(data) self.book_info.show_data(data)
self.cover_view.show_data(data) self.cover_view.show_data(data)
self.current_path = data.get(_('Path'), '') self.current_path = getattr(data, u'path', u'')
self.update_layout() self.update_layout()
def update_layout(self): def update_layout(self):
@ -500,7 +511,7 @@ class BookDetails(QWidget): # {{{
) )
def reset_info(self): def reset_info(self):
self.show_data({}) self.show_data(Metadata(_('Unknown')))
# }}} # }}}

View File

@ -3,30 +3,33 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import textwrap, os, re
from PyQt4.Qt import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, \ from PyQt4.Qt import (QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt,
QDialog, QPixmap, QIcon, QSize QDialog, QPixmap, QIcon, QSize, QPalette)
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
from calibre.gui2 import dynamic, open_local_file, open_url from calibre.gui2 import dynamic
from calibre import fit_image from calibre import fit_image
from calibre.library.comments import comments_to_html from calibre.gui2.book_details import render_html
from calibre.utils.icu import sort_key
class BookInfo(QDialog, Ui_BookInfo): class BookInfo(QDialog, Ui_BookInfo):
def __init__(self, parent, view, row, view_func): def __init__(self, parent, view, row, link_delegate):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
Ui_BookInfo.__init__(self) Ui_BookInfo.__init__(self)
self.setupUi(self) self.setupUi(self)
self.gui = parent self.gui = parent
self.cover_pixmap = None self.cover_pixmap = None
self.comments.sizeHint = self.comments_size_hint self.details.sizeHint = self.details_size_hint
self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks) self.details.page().setLinkDelegationPolicy(self.details.page().DelegateAllLinks)
self.comments.linkClicked.connect(self.link_clicked) self.details.linkClicked.connect(self.link_clicked)
self.view_func = view_func self.css = P('templates/book_details.css', data=True).decode('utf-8')
self.link_delegate = link_delegate
self.details.setAttribute(Qt.WA_OpaquePaintEvent, False)
palette = self.details.palette()
self.details.setAcceptDrops(False)
palette.setBrush(QPalette.Base, Qt.transparent)
self.details.page().setPalette(palette)
self.view = view self.view = view
@ -37,7 +40,6 @@ class BookInfo(QDialog, Ui_BookInfo):
self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave) self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave)
self.connect(self.next_button, SIGNAL('clicked()'), self.next) self.connect(self.next_button, SIGNAL('clicked()'), self.next)
self.connect(self.previous_button, SIGNAL('clicked()'), self.previous) self.connect(self.previous_button, SIGNAL('clicked()'), self.previous)
self.connect(self.text, SIGNAL('linkActivated(QString)'), self.open_book_path)
self.fit_cover.stateChanged.connect(self.toggle_cover_fit) self.fit_cover.stateChanged.connect(self.toggle_cover_fit)
self.cover.resizeEvent = self.cover_view_resized self.cover.resizeEvent = self.cover_view_resized
self.cover.cover_changed.connect(self.cover_changed) self.cover.cover_changed.connect(self.cover_changed)
@ -46,6 +48,10 @@ class BookInfo(QDialog, Ui_BookInfo):
screen_height = desktop.availableGeometry().height() - 100 screen_height = desktop.availableGeometry().height() - 100
self.resize(self.size().width(), screen_height) self.resize(self.size().width(), screen_height)
def link_clicked(self, qurl):
link = unicode(qurl.toString())
self.link_delegate(link)
def cover_changed(self, data): def cover_changed(self, data):
if self.current_row is not None: if self.current_row is not None:
id_ = self.view.model().id(self.current_row) id_ = self.view.model().id(self.current_row)
@ -60,11 +66,8 @@ class BookInfo(QDialog, Ui_BookInfo):
if self.fit_cover.isChecked(): if self.fit_cover.isChecked():
self.resize_cover() self.resize_cover()
def link_clicked(self, url): def details_size_hint(self):
open_url(url) return QSize(350, 550)
def comments_size_hint(self):
return QSize(350, 250)
def toggle_cover_fit(self, state): def toggle_cover_fit(self, state):
dynamic.set('book_info_dialog_fit_cover', self.fit_cover.isChecked()) dynamic.set('book_info_dialog_fit_cover', self.fit_cover.isChecked())
@ -77,13 +80,6 @@ class BookInfo(QDialog, Ui_BookInfo):
row = current.row() row = current.row()
self.refresh(row) self.refresh(row)
def open_book_path(self, path):
path = unicode(path)
if os.sep in path:
open_local_file(path)
else:
self.view_func(self.view.model().id(self.current_row), path)
def next(self): def next(self):
row = self.view.currentIndex().row() row = self.view.currentIndex().row()
ni = self.view.model().index(row+1, 0) ni = self.view.model().index(row+1, 0)
@ -117,8 +113,8 @@ class BookInfo(QDialog, Ui_BookInfo):
row = row.row() row = row.row()
if row == self.current_row: if row == self.current_row:
return return
info = self.view.model().get_book_info(row) mi = self.view.model().get_book_display_info(row)
if info is None: if mi is None:
# Indicates books was deleted from library, or row numbers have # Indicates books was deleted from library, or row numbers have
# changed # changed
return return
@ -126,40 +122,11 @@ class BookInfo(QDialog, Ui_BookInfo):
self.previous_button.setEnabled(False if row == 0 else True) self.previous_button.setEnabled(False if row == 0 else True)
self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True) self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
self.current_row = row self.current_row = row
self.setWindowTitle(info[_('Title')]) self.setWindowTitle(mi.title)
self.title.setText('<b>'+info.pop(_('Title'))) self.title.setText('<b>'+mi.title)
comments = info.pop(_('Comments'), '') mi.title = _('Unknown')
if comments: self.cover_pixmap = QPixmap.fromImage(mi.cover_data[1])
comments = comments_to_html(comments)
if re.search(r'<[a-zA-Z]+>', comments) is None:
lines = comments.splitlines()
lines = [x if x.strip() else '<br><br>' for x in lines]
comments = '\n'.join(lines)
self.comments.setHtml('<div>%s</div>' % comments)
self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks)
cdata = info.pop('cover', '')
self.cover_pixmap = QPixmap.fromImage(cdata)
self.resize_cover() self.resize_cover()
html = render_html(mi, self.css, True, self, all_fields=True)
self.details.setHtml(html)
rows = u''
self.text.setText('')
self.data = info
if _('Path') in info.keys():
p = info[_('Path')]
info[_('Path')] = '<a href="%s">%s</a>'%(p, p)
if _('Formats') in info.keys():
formats = info[_('Formats')].split(',')
info[_('Formats')] = ''
for f in formats:
f = f.strip()
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
for key in sorted(info.keys(), key=sort_key):
if key == 'id': continue
txt = info[key]
if key.endswith(':html'):
key = key[:-5]
txt = comments_to_html(txt)
if key != _('Path'):
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
self.text.setText(u'<table>'+rows+'</table>')

View File

@ -20,6 +20,12 @@
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2"> <item row="0" column="0" colspan="2">
<widget class="QLabel" name="title"> <widget class="QLabel" name="title">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font"> <property name="font">
<font> <font>
<weight>75</weight> <weight>75</weight>
@ -34,82 +40,17 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0" rowspan="3"> <item row="2" column="0" rowspan="3">
<widget class="CoverView" name="cover"/> <widget class="CoverView" name="cover"/>
</item> </item>
<item row="1" column="1"> <item row="3" column="1">
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>435</width>
<height>670</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="text">
<property name="text">
<string>TextLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Comments</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWebView" name="comments">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>350</width>
<height>16777215</height>
</size>
</property>
<property name="url">
<url>
<string>about:blank</string>
</url>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="fit_cover"> <widget class="QCheckBox" name="fit_cover">
<property name="text"> <property name="text">
<string>Fit &amp;cover within view</string> <string>Fit &amp;cover within view</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QPushButton" name="previous_button"> <widget class="QPushButton" name="previous_button">
@ -135,6 +76,15 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="2" column="1">
<widget class="QWebView" name="details">
<property name="url">
<url>
<string>about:blank</string>
</url>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<customwidgets> <customwidgets>

View File

@ -8,20 +8,20 @@ __docformat__ = 'restructuredtext en'
import shutil, functools, re, os, traceback import shutil, functools, re, os, traceback
from contextlib import closing from contextlib import closing
from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \ from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage,
QModelIndex, QVariant, QDate, QColor QModelIndex, QVariant, QDate, QColor)
from calibre.gui2 import NONE, config, UNDEFINED_QDATE from calibre.gui2 import NONE, UNDEFINED_QDATE
from calibre.utils.pyparsing import ParseException from calibre.utils.pyparsing import ParseException
from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import tweaks, prefs from calibre.utils.config import tweaks, prefs
from calibre.utils.date import dt_factory, qt_to_dt, isoformat from calibre.utils.date import dt_factory, qt_to_dt
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ from calibre.library.caches import (_match, CONTAINS_MATCH, EQUALS_MATCH,
REGEXP_MATCH, MetadataBackup, force_to_bool REGEXP_MATCH, MetadataBackup, force_to_bool)
from calibre import strftime, isbytestring, prepare_string_for_xml from calibre import strftime, isbytestring
from calibre.constants import filesystem_encoding, DEBUG from calibre.constants import filesystem_encoding, DEBUG
from calibre.gui2.library import DEFAULT_SORT from calibre.gui2.library import DEFAULT_SORT
@ -114,7 +114,7 @@ class BooksModel(QAbstractTableModel): # {{{
return cc_label in self.custom_columns return cc_label in self.custom_columns
def read_config(self): def read_config(self):
self.use_roman_numbers = config['use_roman_numerals_for_series_number'] pass
def set_device_connected(self, is_connected): def set_device_connected(self, is_connected):
self.device_connected = is_connected self.device_connected = is_connected
@ -355,63 +355,13 @@ class BooksModel(QAbstractTableModel): # {{{
return self.rowCount(None) return self.rowCount(None)
def get_book_display_info(self, idx): def get_book_display_info(self, idx):
def custom_keys_to_display():
ans = getattr(self, '_custom_fields_in_book_info', None)
if ans is None:
cfkeys = set(self.db.custom_field_keys())
yes_fields = set(tweaks['book_details_will_display'])
no_fields = set(tweaks['book_details_wont_display'])
if '*' in yes_fields:
yes_fields = cfkeys
if '*' in no_fields:
no_fields = cfkeys
ans = frozenset(yes_fields - no_fields)
setattr(self, '_custom_fields_in_book_info', ans)
return ans
data = {}
cdata = self.cover(idx)
if cdata:
data['cover'] = cdata
tags = list(self.db.get_tags(self.db.id(idx)))
if tags:
tags.sort(key=sort_key)
tags = ', '.join(tags)
else:
tags = _('None')
data[_('Tags')] = tags
formats = self.db.formats(idx)
if formats:
formats = formats.replace(',', ', ')
else:
formats = _('None')
data[_('Formats')] = formats
data[_('Path')] = self.db.abspath(idx)
data['id'] = self.id(idx)
comments = self.db.comments(idx)
if not comments:
comments = _('None')
data[_('Comments')] = comments
series = self.db.series(idx)
if series:
sidx = self.db.series_index(idx)
sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers)
data[_('Series')] = \
_('Book %s of %s.')%\
(sidx, prepare_string_for_xml(series))
mi = self.db.get_metadata(idx) mi = self.db.get_metadata(idx)
cf_to_display = custom_keys_to_display() mi.size = mi.book_size
for key in mi.custom_field_keys(): mi.cover_data = ('jpg', self.cover(idx))
if key not in cf_to_display: mi.id = self.db.id(idx)
continue mi.field_metadata = self.db.field_metadata
name, val = mi.format_field(key) mi.path = self.db.abspath(idx, create_dirs=False)
if mi.metadata_for_field(key)['datatype'] == 'comments': return mi
name += ':html'
if val and name not in data:
data[name] = val
return data
def current_changed(self, current, previous, emit_signal=True): def current_changed(self, current, previous, emit_signal=True):
if current.isValid(): if current.isValid():
@ -425,16 +375,8 @@ class BooksModel(QAbstractTableModel): # {{{
def get_book_info(self, index): def get_book_info(self, index):
if isinstance(index, int): if isinstance(index, int):
index = self.index(index, 0) index = self.index(index, 0)
# If index is not valid returns None
data = self.current_changed(index, None, False) data = self.current_changed(index, None, False)
if data is None:
return data
row = index.row()
data[_('Title')] = self.db.title(row)
au = self.db.authors(row)
if not au:
au = _('Unknown')
au = authors_to_string([a.strip().replace('|', ',') for a in au.split(',')])
data[_('Author(s)')] = au
return data return data
def metadata_for(self, ids): def metadata_for(self, ids):
@ -1189,39 +1131,46 @@ class DeviceBooksModel(BooksModel): # {{{
img = self.default_image img = self.default_image
return img return img
def current_changed(self, current, previous): def get_book_display_info(self, idx):
data = {} from calibre.ebooks.metadata.book.base import Metadata
item = self.db[self.map[current.row()]] item = self.db[self.map[idx]]
cover = self.cover(current.row()) cover = self.cover(idx)
if cover is not self.default_image: if cover is self.default_image:
data['cover'] = cover cover = None
type = _('Unknown') title = item.title
if not title:
title = _('Unknown')
au = item.authors
if not au:
au = [_('Unknown')]
mi = Metadata(title, au)
mi.cover_data = ('jpg', cover)
fmt = _('Unknown')
ext = os.path.splitext(item.path)[1] ext = os.path.splitext(item.path)[1]
if ext: if ext:
type = ext[1:].lower() fmt = ext[1:].lower()
data[_('Format')] = type mi.formats = [fmt]
data[_('Path')] = item.path mi.path = (item.path if item.path else None)
dt = dt_factory(item.datetime, assume_utc=True) dt = dt_factory(item.datetime, assume_utc=True)
data[_('Timestamp')] = isoformat(dt, sep=' ', as_utc=False) mi.timestamp = dt
data[_('Collections')] = ', '.join(item.device_collections) mi.device_collections = list(item.device_collections)
mi.tags = list(getattr(item, 'tags', []))
tags = getattr(item, 'tags', None) mi.comments = getattr(item, 'comments', None)
if tags:
tags = u', '.join(tags)
else:
tags = _('None')
data[_('Tags')] = tags
comments = getattr(item, 'comments', None)
if not comments:
comments = _('None')
data[_('Comments')] = comments
series = getattr(item, 'series', None) series = getattr(item, 'series', None)
if series: if series:
sidx = getattr(item, 'series_index', 0) sidx = getattr(item, 'series_index', 0)
sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers) mi.series = series
data[_('Series')] = _('Book <font face="serif">%s</font> of %s.')%(sidx, series) mi.series_index = sidx
return mi
def current_changed(self, current, previous, emit_signal=True):
if current.isValid():
idx = current.row()
data = self.get_book_display_info(idx)
if emit_signal:
self.new_bookdisplay_data.emit(data) self.new_bookdisplay_data.emit(data)
else:
return data
def paths(self, rows): def paths(self, rows):
return [self.db[self.map[r.row()]].path for r in rows ] return [self.db[self.map[r.row()]].path for r in rows ]
@ -1281,7 +1230,7 @@ class DeviceBooksModel(BooksModel): # {{{
elif cname == 'authors': elif cname == 'authors':
au = self.db[self.map[row]].authors au = self.db[self.map[row]].authors
if not au: if not au:
au = self.unknown au = [_('Unknown')]
return QVariant(authors_to_string(au)) return QVariant(authors_to_string(au))
elif cname == 'size': elif cname == 'size':
size = self.db[self.map[row]].size size = self.db[self.map[row]].size

View File

@ -650,6 +650,11 @@ class BooksView(QTableView): # {{{
def column_map(self): def column_map(self):
return self._model.column_map return self._model.column_map
def refresh_book_details(self):
idx = self.currentIndex()
if idx.isValid():
self._model.current_changed(idx, idx)
def scrollContentsBy(self, dx, dy): def scrollContentsBy(self, dx, dy):
# Needed as Qt bug causes headerview to not always update when scrolling # Needed as Qt bug causes headerview to not always update when scrolling
QTableView.scrollContentsBy(self, dx, dy) QTableView.scrollContentsBy(self, dx, dy)

View File

@ -5,15 +5,91 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog,
QAbstractListModel, Qt)
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2.preferences.look_feel_ui import Ui_Form
from calibre.gui2 import config, gprefs, qt_app from calibre.gui2 import config, gprefs, qt_app
from calibre.utils.localization import available_translations, \ from calibre.utils.localization import (available_translations,
get_language, get_lang get_language, get_lang)
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.gui2 import NONE
from calibre.gui2.book_details import get_field_list
class DisplayedFields(QAbstractListModel): # {{{
def __init__(self, db, parent=None):
QAbstractListModel.__init__(self, parent)
self.fields = []
self.db = db
self.changed = False
def initialize(self, use_defaults=False):
self.fields = [[x[0], x[1]] for x in
get_field_list(self.db.field_metadata,
use_defaults=use_defaults)]
self.reset()
self.changed = True
def rowCount(self, *args):
return len(self.fields)
def data(self, index, role):
try:
field, visible = self.fields[index.row()]
except:
return NONE
if role == Qt.DisplayRole:
name = field
try:
name = self.db.field_metadata[field]['name']
except:
pass
if not name:
name = field
return name
if role == Qt.CheckStateRole:
return Qt.Checked if visible else Qt.Unchecked
return NONE
def flags(self, index):
ans = QAbstractListModel.flags(self, index)
return ans | Qt.ItemIsUserCheckable
def setData(self, index, val, role):
ret = False
if role == Qt.CheckStateRole:
val, ok = val.toInt()
if ok:
self.fields[index.row()][1] = bool(val)
self.changed = True
ret = True
self.dataChanged.emit(index, index)
return ret
def restore_defaults(self):
self.initialize(use_defaults=True)
def commit(self):
if self.changed:
gprefs['book_display_fields'] = self.fields
def move(self, idx, delta):
row = idx.row() + delta
if row >= 0 and row < len(self.fields):
t = self.fields[row]
self.fields[row] = self.fields[row-delta]
self.fields[row-delta] = t
self.dataChanged.emit(idx, idx)
idx = self.index(row)
self.dataChanged.emit(idx, idx)
self.changed = True
return idx
# }}}
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
@ -76,11 +152,18 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.current_font = self.initial_font = None self.current_font = self.initial_font = None
self.change_font_button.clicked.connect(self.change_font) self.change_font_button.clicked.connect(self.change_font)
self.display_model = DisplayedFields(self.gui.current_db,
self.field_display_order)
self.display_model.dataChanged.connect(self.changed_signal)
self.field_display_order.setModel(self.display_model)
self.df_up_button.clicked.connect(self.move_df_up)
self.df_down_button.clicked.connect(self.move_df_down)
def initialize(self): def initialize(self):
ConfigWidgetBase.initialize(self) ConfigWidgetBase.initialize(self)
self.current_font = self.initial_font = gprefs['font'] self.current_font = self.initial_font = gprefs['font']
self.update_font_display() self.update_font_display()
self.display_model.initialize()
def restore_defaults(self): def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self) ConfigWidgetBase.restore_defaults(self)
@ -89,6 +172,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if ofont is not None: if ofont is not None:
self.changed_signal.emit() self.changed_signal.emit()
self.update_font_display() self.update_font_display()
self.display_model.restore_defaults()
self.changed_signal.emit()
def build_font_obj(self): def build_font_obj(self):
font_info = self.current_font font_info = self.current_font
@ -107,6 +192,24 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.font_display.setText(name + self.font_display.setText(name +
' [%dpt]'%fi.pointSize()) ' [%dpt]'%fi.pointSize())
def move_df_up(self):
idx = self.field_display_order.currentIndex()
if idx.isValid():
idx = self.display_model.move(idx, -1)
if idx is not None:
sm = self.field_display_order.selectionModel()
sm.select(idx, sm.ClearAndSelect)
self.field_display_order.setCurrentIndex(idx)
def move_df_down(self):
idx = self.field_display_order.currentIndex()
if idx.isValid():
idx = self.display_model.move(idx, 1)
if idx is not None:
sm = self.field_display_order.selectionModel()
sm.select(idx, sm.ClearAndSelect)
self.field_display_order.setCurrentIndex(idx)
def change_font(self, *args): def change_font(self, *args):
fd = QFontDialog(self.build_font_obj(), self) fd = QFontDialog(self.build_font_obj(), self)
if fd.exec_() == fd.Accepted: if fd.exec_() == fd.Accepted:
@ -123,12 +226,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
gprefs['font'] = self.current_font gprefs['font'] = self.current_font
QApplication.setFont(self.font_display.font()) QApplication.setFont(self.font_display.font())
rr = True rr = True
self.display_model.commit()
return rr return rr
def refresh_gui(self, gui): def refresh_gui(self, gui):
self.update_font_display() self.update_font_display()
gui.tags_view.reread_collapse_parameters() gui.tags_view.reread_collapse_parameters()
gui.library_view.refresh_book_details()
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication([]) app = QApplication([])

View File

@ -14,6 +14,21 @@
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QToolBox" name="toolBox">
<widget class="QWidget" name="page">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>699</width>
<height>306</height>
</rect>
</property>
<attribute name="label">
<string>Main interface</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label_17"> <widget class="QLabel" name="label_17">
<property name="text"> <property name="text">
@ -41,19 +56,6 @@
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Number of covers to show in browse mode (needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_cover_flow_queue_length</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="opt_cover_flow_queue_length"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7"> <widget class="QLabel" name="label_7">
<property name="text"> <property name="text">
<string>Choose &amp;language (requires restart):</string> <string>Choose &amp;language (requires restart):</string>
@ -63,7 +65,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="1" column="1">
<widget class="QComboBox" name="opt_language"> <widget class="QComboBox" name="opt_language">
<property name="sizeAdjustPolicy"> <property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum> <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
@ -73,17 +75,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="2" column="1">
<widget class="QCheckBox" name="opt_show_avg_rating">
<property name="text">
<string>Show &amp;average ratings in the tags browser</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="opt_disable_animations"> <widget class="QCheckBox" name="opt_disable_animations">
<property name="toolTip"> <property name="toolTip">
<string>Disable all animations. Useful if you have a slow/old computer.</string> <string>Disable all animations. Useful if you have a slow/old computer.</string>
@ -93,45 +85,129 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="3" column="1">
<widget class="QCheckBox" name="opt_systray_icon">
<property name="text">
<string>Enable system &amp;tray icon (needs restart)</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="opt_show_splash_screen"> <widget class="QCheckBox" name="opt_show_splash_screen">
<property name="text"> <property name="text">
<string>Show &amp;splash screen at startup</string> <string>Show &amp;splash screen at startup</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="5" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Icon size:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_icon_size</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_text</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="8" column="0">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Interface font:</string>
</property>
<property name="buddy">
<cstring>font_display</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="font_display">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_systray_icon">
<property name="text">
<string>Enable system &amp;tray icon (needs restart)</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="opt_disable_tray_notification"> <widget class="QCheckBox" name="opt_disable_tray_notification">
<property name="text"> <property name="text">
<string>Disable &amp;notifications in system tray</string> <string>Disable &amp;notifications in system tray</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> </layout>
<widget class="QCheckBox" name="opt_use_roman_numerals_for_series_number"> </widget>
<widget class="QWidget" name="page_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>649</width>
<height>96</height>
</rect>
</property>
<attribute name="label">
<string>Tag Browser</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_show_avg_rating">
<property name="text"> <property name="text">
<string>Use &amp;Roman numerals for series</string> <string>Show &amp;average ratings in the tags browser</string>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0" colspan="2"> <item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="opt_separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<layout class="QHBoxLayout"> <layout class="QHBoxLayout">
<item> <item>
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
@ -190,7 +266,7 @@ up into sub-categories. If the partition method is set to disable, this value is
</item> </item>
</layout> </layout>
</item> </item>
<item row="8" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_81"> <widget class="QLabel" name="label_81">
<property name="text"> <property name="text">
<string>Categories with &amp;hierarchical items:</string> <string>Categories with &amp;hierarchical items:</string>
@ -200,7 +276,7 @@ up into sub-categories. If the partition method is set to disable, this value is
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="1"> <item row="2" column="1">
<widget class="MultiCompleteLineEdit" name="opt_categories_using_hierarchy"> <widget class="MultiCompleteLineEdit" name="opt_categories_using_hierarchy">
<property name="toolTip"> <property name="toolTip">
<string>A comma-separated list of columns in which items containing <string>A comma-separated list of columns in which items containing
@ -212,71 +288,8 @@ then the tags will be displayed each on their own line.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="15" column="0" colspan="2"> <item row="3" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2"> <spacer name="verticalSpacer_2">
<property name="title">
<string>&amp;Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Icon size:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_icon_size</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_text</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="16" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Interface font:</string>
</property>
<property name="buddy">
<cstring>font_display</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="font_display">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="16" column="1">
<widget class="QPushButton" name="change_font_button">
<property name="text">
<string>Change &amp;font (needs restart)</string>
</property>
</widget>
</item>
<item row="17" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
@ -290,6 +303,144 @@ then the tags will be displayed each on their own line.</string>
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="page_3">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>429</width>
<height>63</height>
</rect>
</property>
<attribute name="label">
<string>Cover Browser</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="opt_separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>&amp;Number of covers to show in browse mode (needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_cover_flow_queue_length</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="opt_cover_flow_queue_length"/>
</item>
<item row="2" column="0" colspan="2">
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_4">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>699</width>
<height>306</height>
</rect>
</property>
<attribute name="label">
<string>Book Details</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="1">
<widget class="QCheckBox" name="opt_use_roman_numerals_for_series_number">
<property name="text">
<string>Use &amp;Roman numerals for series</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" rowspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Select displayed metadata</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0" rowspan="3">
<widget class="QListView" name="field_display_order">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QToolButton" name="df_up_button">
<property name="toolTip">
<string>Move up</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-up.png</normaloff>:/images/arrow-up.png</iconset>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QToolButton" name="df_down_button">
<property name="toolTip">
<string>Move down</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-down.png</normaloff>:/images/arrow-down.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Note that &lt;b&gt;comments&lt;/b&gt; will always be displayed at the end, regardless of the position you assign here.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>
<class>MultiCompleteLineEdit</class> <class>MultiCompleteLineEdit</class>
@ -297,6 +448,8 @@ then the tags will be displayed each on their own line.</string>
<header>calibre/gui2/complete.h</header> <header>calibre/gui2/complete.h</header>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources/> <resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections/> <connections/>
</ui> </ui>

View File

@ -161,18 +161,6 @@ class StorePlugin(object): # {{{
''' '''
return False return False
def get_settings(self):
'''
This is only useful for plugins that implement
:attr:`config_widget` that is the only way to save
settings. This is used by plugins to get the saved
settings and apply when necessary.
:return: A dictionary filled with the settings used
by this plugin.
'''
raise NotImplementedError()
def do_genesis(self): def do_genesis(self):
self.genesis() self.genesis()

View File

@ -24,10 +24,9 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class BaenWebScriptionStore(BasicStoreConfig, StorePlugin): class BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.webscription.net/' url = 'http://www.webscription.net/'
if external or settings.get(self.name + '_open_external', False): if external or self.config.get('open_external', False):
if detail_item: if detail_item:
url = url + detail_item url = url + detail_item
open_url(QUrl(url_slash_cleaner(url))) open_url(QUrl(url_slash_cleaner(url)))
@ -37,7 +36,7 @@ class BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item detail_url = url + detail_item
d = WebStoreDialog(self.gui, url, parent, detail_url) d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):

View File

@ -37,7 +37,7 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
d.set_tags(self.config.get('tags', '')) d.set_tags(self.config.get('tags', ''))
d.exec_() d.exec_()
else: else:
if self.update_cache(parent, 30): self.update_cache(parent, 30)
d = MobeReadStoreDialog(self, parent) d = MobeReadStoreDialog(self, parent)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.exec_() d.exec_()

View File

@ -206,7 +206,7 @@ class SearchDialog(QDialog, Ui_Dialog):
if res: if res:
self.results_view.model().add_result(res, store_plugin) self.results_view.model().add_result(res, store_plugin)
if not self.results_view.model().has_results(): if not self.search_pool.threads_running() and not self.results_view.model().has_results():
info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False) info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False)
@ -247,5 +247,6 @@ class SearchDialog(QDialog, Ui_Dialog):
def dialog_closed(self, result): def dialog_closed(self, result):
self.results_view.model().closing() self.results_view.model().closing()
self.search_pool.abort() self.search_pool.abort()
self.cache_pool.abort()
self.save_state() self.save_state()

View File

@ -121,7 +121,7 @@
<addaction name="action_font_size_larger"/> <addaction name="action_font_size_larger"/>
<addaction name="action_font_size_smaller"/> <addaction name="action_font_size_smaller"/>
<addaction name="action_table_of_contents"/> <addaction name="action_table_of_contents"/>
<addaction name="action_metadata"/> <addaction name="action_full_screen"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_previous_page"/> <addaction name="action_previous_page"/>
<addaction name="action_next_page"/> <addaction name="action_next_page"/>
@ -130,7 +130,7 @@
<addaction name="action_reference_mode"/> <addaction name="action_reference_mode"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_preferences"/> <addaction name="action_preferences"/>
<addaction name="action_full_screen"/> <addaction name="action_metadata"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="action_print"/> <addaction name="action_print"/>
</widget> </widget>

View File

@ -1,9 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -1,37 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
''' Design documentation {{{
Storage paradigm {{{
* Agnostic to storage paradigm (i.e. no book per folder assumptions)
* Two separate concepts: A store and collection
A store is a backend, like a sqlite database associated with a path on
the local filesystem, or a cloud based storage solution.
A collection is a user defined group of stores. Most of the logic for
data manipulation sorting/searching/restrictions should be in the collection
class. The collection class should transparently handle the
conversion from store name + id to row number in the collection.
* Not sure how feasible it is to allow many-many maps between stores
and collections.
}}}
Event system {{{
* Comprehensive event system that other components can subscribe to
* Subscribers should be able to temporarily block receiving events
* Should event dispatch be asynchronous?
* Track last modified time for metadata and each format
}}}
}}}'''
# Imports {{{
# }}}

View File

@ -188,7 +188,7 @@ class FieldMetadata(dict):
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
'name':None, 'name':_('Author Sort'),
'search_terms':['author_sort'], 'search_terms':['author_sort'],
'is_custom':False, 'is_custom':False,
'is_category':False, 'is_category':False,
@ -238,7 +238,7 @@ class FieldMetadata(dict):
'datatype':'datetime', 'datatype':'datetime',
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
'name':_('Date'), 'name':_('Modified'),
'search_terms':['last_modified'], 'search_terms':['last_modified'],
'is_custom':False, 'is_custom':False,
'is_category':False, 'is_category':False,
@ -258,7 +258,7 @@ class FieldMetadata(dict):
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
'name':None, 'name':_('Path'),
'search_terms':[], 'search_terms':[],
'is_custom':False, 'is_custom':False,
'is_category':False, 'is_category':False,
@ -308,7 +308,7 @@ class FieldMetadata(dict):
'datatype':'float', 'datatype':'float',
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
'name':_('Size (MB)'), 'name':_('Size'),
'search_terms':['size'], 'search_terms':['size'],
'is_custom':False, 'is_custom':False,
'is_category':False, 'is_category':False,
@ -399,6 +399,13 @@ class FieldMetadata(dict):
if self._tb_cats[k]['kind']=='field' and if self._tb_cats[k]['kind']=='field' and
self._tb_cats[k]['datatype'] is not None] self._tb_cats[k]['datatype'] is not None]
def displayable_field_keys(self):
return [k for k in self._tb_cats.keys()
if self._tb_cats[k]['kind']=='field' and
self._tb_cats[k]['datatype'] is not None and
k not in ('au_map', 'marked', 'ondevice', 'cover') and
not self.is_series_index(k)]
def standard_field_keys(self): def standard_field_keys(self):
return [k for k in self._tb_cats.keys() return [k for k in self._tb_cats.keys()
if self._tb_cats[k]['kind']=='field' and if self._tb_cats[k]['kind']=='field' and
@ -442,6 +449,11 @@ class FieldMetadata(dict):
def is_custom_field(self, key): def is_custom_field(self, key):
return key.startswith(self.custom_field_prefix) return key.startswith(self.custom_field_prefix)
def is_series_index(self, key):
m = self[key]
return (m['datatype'] == 'float' and key.endswith('_index') and
key[:-6] in self)
def key_to_label(self, key): def key_to_label(self, key):
if 'label' not in self._tb_cats[key]: if 'label' not in self._tb_cats[key]:
return key return key

View File

@ -149,7 +149,8 @@ class PostInstall:
if islinux or isfreebsd: if islinux or isfreebsd:
for f in os.listdir('.'): for f in os.listdir('.'):
if os.stat(f).st_uid == 0: if os.stat(f).st_uid == 0:
os.rmdir(f) if os.path.isdir(f) else os.unlink(f) import shutil
shutil.rmtree(f) if os.path.isdir(f) else os.unlink(f)
if os.stat(config_dir).st_uid == 0: if os.stat(config_dir).st_uid == 0:
os.rmdir(config_dir) os.rmdir(config_dir)

View File

@ -20,12 +20,14 @@ What formats does |app| support conversion to/from?
|app| supports the conversion of many input formats to many output formats. |app| supports the conversion of many input formats to many output formats.
It can convert every input format in the following list, to every output format. It can convert every input format in the following list, to every output format.
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB, PML, RB, RTF, SNB, TCR, TXT, TXTZ *Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB***, PML, RB, RTF, SNB, TCR, TXT, TXTZ
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, HTMLZ, PDB, PML, RB, PDF, SNB, TCR, TXT, TXTZ *Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, HTMLZ, PDB, PML, RB, PDF, SNB, TCR, TXT, TXTZ
** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers ** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers
*** PDB is also a generic format. |app| supports eReder, Plucker, PML and zTxt PDB files.
.. _best-source-formats: .. _best-source-formats:
What are the best source formats to convert? What are the best source formats to convert?