mirror of
git://github.com/kovidgoyal/calibre.git
synced 2026-05-04 21:03:37 +02:00
merge from trunk
This commit is contained in:
commit
984a7510fc
102 changed files with 125800 additions and 73329 deletions
116
Changelog.yaml
116
Changelog.yaml
|
|
@ -4,6 +4,122 @@
|
|||
# for important features/bug fixes.
|
||||
# Also, each release can have new and improved recipes.
|
||||
|
||||
- version: 0.7.42
|
||||
date: 2011-01-21
|
||||
|
||||
new features:
|
||||
- title: "0.7.42 is a re-release of 0.7.41, because conversion to MOBI was broken in 0.7.41"
|
||||
|
||||
- title: "Conversions: Replace the remove header/footer options with a more geenric search replace option, that allows you to not only remove but also replace text"
|
||||
|
||||
- title: "Conversion: The preprocess html option has now become a new 'Heuristic Processing' option which allows you to control exactly which heuristics are used"
|
||||
|
||||
- title: "Conversion: Various improvements to Heuristic Processing (used to be preprocess HTML)"
|
||||
|
||||
- title: "When adding empty books to calibre, optionally set the author to the author of the currently selected book"
|
||||
tickets: [7702]
|
||||
|
||||
- title: "Device drivers for the Archos 101, SmatQ T7 and Acer Lumiread"
|
||||
|
||||
- title: "Catalog generation: Make By Authors optional"
|
||||
|
||||
- title: "Allow bulk editing of Date and Published columns."
|
||||
|
||||
- title: "Add a little button to clear date and published values to the edit metadata dialogs"
|
||||
|
||||
- title: "When adding books by ISBN, allow the specification of special tags that will be added to the new book entries"
|
||||
tickets: [8436]
|
||||
|
||||
- title: "Completion on multiple authors"
|
||||
tickets: [8405]
|
||||
|
||||
- title: "Add AZW to default list of internally viewed formats, a I am tired of getting tickets about it"
|
||||
|
||||
- title: "Nicer error message when catalog generation fails"
|
||||
|
||||
- title: "Add capitalize option to context menus in the edit metadata dialog"
|
||||
|
||||
bug fixes:
|
||||
- title: "RTF Input: Fix regression in 0.7.40 that broke conversion of some old style RTF files"
|
||||
|
||||
- title: "Fix Tag editor forgets position"
|
||||
tickets: [8271]
|
||||
|
||||
- title: "When converting books in the calibre GUI, override metadata from the input document, even when empty."
|
||||
description: >
|
||||
"So if you have removed all the tags and comments in the calibre GUI for the book in the calibre GUI, but the actual file that is being converted still has tags and comments, they are ignored. This affects only conversions in the calibre GUI, not from the command line via ebook-convert."
|
||||
tickets: [8390]
|
||||
|
||||
- title: "Fix memory leak when switching libraries"
|
||||
|
||||
- title: "RTF Output: Fix incorrent spacing between letters."
|
||||
tickets: [8422]
|
||||
|
||||
- title: "Catalog generation: Add composite columns to Merge Comments eligible types"
|
||||
|
||||
- title: "Add a confirmation when closing the add a custom news source dialog."
|
||||
tickets: [8460]
|
||||
|
||||
- title: "Another workaround for LibraryThing UA sniffing that was preventing series metadata download, sigh."
|
||||
tickets: [8477]
|
||||
|
||||
- title: "PD Novel driver: Put books on the SD card into the eBooks folder"
|
||||
|
||||
- title: "When shortening filepaths to conform to windows path length limitations, remove text from the middle of each component instead of the ends."
|
||||
tickets: [8451]
|
||||
|
||||
- title: "Make completion in most places case insensitive"
|
||||
tickets: [8441]
|
||||
|
||||
- title: "Fix regression that caused the N key to stop working when editing a Yes/no column"
|
||||
tickets: [8417]
|
||||
|
||||
- title: "Email: Fix bug when connecting to SMTP relays that use MD5 auth"
|
||||
|
||||
- title: "MOBI Output: Fix bug that could cause a link pointing to the start of a section to go to a point later in the section is the section contained an empty id attribute"
|
||||
|
||||
- title: "When auto converting books and the device is unplugged, do not raise an error."
|
||||
tickets: [8426]
|
||||
|
||||
- title: "Ebook-viewer: Display cover when viewing FB2 files"
|
||||
|
||||
- title: "MOBI Input: Special case handling of emptu div tags with a defined height used as paragraph separators."
|
||||
tickets: [8391]
|
||||
|
||||
- title: "Fix sorting of author names into sub categories by first letter in the Tag Browser when the first letter has diacritics"
|
||||
tickets: [8378]
|
||||
|
||||
- title: "Fix regression in 0.7.40 that caused commas in author names to become | when converting/saving to disk"
|
||||
|
||||
- title: "Fix view specific format on a book with no formats gives an error"
|
||||
tickets: [8352]
|
||||
|
||||
|
||||
improved recipes:
|
||||
- Blic
|
||||
- Las Vegas Review Journal
|
||||
- La Vanguardia
|
||||
- New York Times
|
||||
- El Pais
|
||||
- Seattle Times
|
||||
- Ars Technica
|
||||
- Dilbert
|
||||
- Nature News
|
||||
|
||||
new recipes:
|
||||
- title: "kath.net"
|
||||
author: "Bobus"
|
||||
|
||||
- title: "iHNed"
|
||||
author: "Karel Bilek"
|
||||
|
||||
- title: "Gulf News"
|
||||
author: "Darko Miletic"
|
||||
|
||||
- title: "South Africa Mail and Guardian"
|
||||
author: "77ja65"
|
||||
|
||||
|
||||
- version: 0.7.40
|
||||
date: 2011-01-14
|
||||
|
||||
|
|
|
|||
36
resources/recipes/everett_herald.recipe
Normal file
36
resources/recipes/everett_herald.recipe
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1295088390(BasicNewsRecipe):
|
||||
title = u'Everett Herald'
|
||||
language = 'en'
|
||||
__author__ = '77ja65'
|
||||
oldest_article = 4
|
||||
max_articles_per_feed = 50
|
||||
no_stylesheets = True
|
||||
masthead_url = 'http://heraldnet.com/images/hnet/jQueryComponents/jQueryNavigation/heraldnet_logo.png'
|
||||
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'
|
||||
|
||||
feeds = [(u'Local News',
|
||||
u'http://heraldnet.com/section/RSS02&mime=xml'),
|
||||
(u'Sports', u'http://heraldnet.com/section/RSS04&mime=xml'),
|
||||
(u'Entertainment',
|
||||
u'http://heraldnet.com/section/RSS07&mime=xml'),
|
||||
(u'Life', u'http://heraldnet.com/section/RSS03&mime=xml'),
|
||||
(u'Breaking News',
|
||||
u'http://heraldnet.com/section/RSS34&mime=xml'),
|
||||
(u'Seahawks', u'http://heraldnet.com/section/RSS22&mime=xml'),
|
||||
(u'HeraldNet', u'http://heraldnet.com/section/RSS01&mime=xml'),
|
||||
(u'Inside Everett',
|
||||
u'http://heraldnet.com/section/RSS26&mime=xml')
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
return url + "&template=PrinterFriendly"
|
||||
|
||||
extra_css = '''
|
||||
h1{font-family:Arial,Helvetica,sans-serif; font-
|
||||
weight:bold;font-size:large;}
|
||||
h2{font-family:Arial,Helvetica,sans-serif; font-
|
||||
weight:normal;font-size:small;}
|
||||
'''
|
||||
|
||||
|
|
@ -5,6 +5,7 @@ class AdvancedUserRecipe1293122276(BasicNewsRecipe):
|
|||
__author__ = 'Jack Mason'
|
||||
author = 'IBM Global Business Services'
|
||||
publisher = 'IBM'
|
||||
language = 'en'
|
||||
category = 'news, technology, IT, internet of things, analytics'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 30
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ class KANewsRecipe(BasicNewsRecipe):
|
|||
description = u'Nachrichten aus Karlsruhe, Deutschland und der Welt.'
|
||||
__author__ = 'tfeld'
|
||||
lang='de'
|
||||
language = 'de'
|
||||
no_stylesheets = True
|
||||
|
||||
oldest_article = 7
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ class AdvancedUserRecipe1295262156(BasicNewsRecipe):
|
|||
title = u'kath.net'
|
||||
__author__ = 'Bobus'
|
||||
oldest_article = 7
|
||||
language = 'en'
|
||||
max_articles_per_feed = 100
|
||||
|
||||
feeds = [(u'kath.net', u'http://www.kath.net/2005/xml/index.xml')]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
class NationalGeographicNews(BasicNewsRecipe):
|
||||
title = u'National Geographic News'
|
||||
oldest_article = 7
|
||||
language = 'en'
|
||||
max_articles_per_feed = 100
|
||||
remove_javascript = True
|
||||
no_stylesheets = True
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
nrc.nl
|
||||
'''
|
||||
|
|
@ -15,13 +15,18 @@ class Pagina12(BasicNewsRecipe):
|
|||
oldest_article = 2
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'cp1252'
|
||||
encoding = 'utf8'
|
||||
use_embedded_content = False
|
||||
language = 'nl'
|
||||
country = 'NL'
|
||||
remove_empty_feeds = True
|
||||
masthead_url = 'http://www.nrc.nl/nrc.nl/images/logo_nrc.png'
|
||||
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} h1,h2,h3{text-align:left} '
|
||||
extra_css = """
|
||||
body{font-family: Georgia,serif }
|
||||
img{margin-bottom: 0.4em; display: block}
|
||||
.bijschrift,.sectie{font-size: x-small}
|
||||
.sectie{color: gray}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
|
|
@ -30,21 +35,42 @@ class Pagina12(BasicNewsRecipe):
|
|||
, 'language' : language
|
||||
}
|
||||
|
||||
keep_only_tags = [dict(name='div',attrs={'class':'article clearfix'})]
|
||||
|
||||
|
||||
keep_only_tags = [dict(attrs={'class':'uitstekendekeus'})]
|
||||
remove_tags = [
|
||||
dict(name=['meta','base','link','object','embed'])
|
||||
,dict(attrs={'class':['reclamespace','tags-and-sharing']})
|
||||
]
|
||||
remove_attributes=['lang']
|
||||
|
||||
feeds = [
|
||||
(u'Voorpagina' , u'http://feeds.feedburner.com/NRCHandelsbladVoorpagina' )
|
||||
,(u'Binnenland' , u'http://feeds.feedburner.com/NRCHandelsbladBinnenland' )
|
||||
,(u'Buitenland' , u'http://feeds.feedburner.com/NRCHandelsbladBuitenland' )
|
||||
,(u'Economie' , u'http://feeds.feedburner.com/NRCHandelsbladEconomie' )
|
||||
,(u'Kunst & Film' , u'http://feeds.feedburner.com/nrc/NRCHandelsbladKunstEnFilm')
|
||||
,(u'Sport' , u'http://feeds.feedburner.com/NRCHandelsbladSport' )
|
||||
,(u'Wetenschap ' , u'http://www.nrc.nl/rss/wetenschap' )
|
||||
(u'Voor nieuws', u'http://www.nrc.nl/nieuws/categorie/nieuws/rss.php' )
|
||||
,(u'Binnenland' , u'http://www.nrc.nl/nieuws/categorie/binnenland/rss.php' )
|
||||
,(u'Buitenland' , u'http://www.nrc.nl/nieuws/categorie/buitenland/rss.php' )
|
||||
,(u'Economie' , u'http://www.nrc.nl/nieuws/categorie/economie/rss.php' )
|
||||
,(u'Cultuur' , u'http://www.nrc.nl/nieuws/categorie/cultuur/rss.php' )
|
||||
,(u'Sport' , u'http://www.nrc.nl/nieuws/categorie/sport/rss.php' )
|
||||
,(u'Wetenschap ', u'http://www.nrc.nl/nieuws/categorie/wetenschap-nieuws/rss.php')
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
return url + '?service=Print'
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for item in soup.findAll('a'):
|
||||
limg = item.find('img')
|
||||
if item.string is not None:
|
||||
str = item.string
|
||||
item.replaceWith(str)
|
||||
else:
|
||||
if limg:
|
||||
item.name = 'div'
|
||||
atritems =['href','target','rel']
|
||||
for atit in atritems:
|
||||
if item.has_key(atit):
|
||||
del item[atit]
|
||||
else:
|
||||
str = self.tag_to_string(item)
|
||||
item.replaceWith(str)
|
||||
for item in soup.findAll('img'):
|
||||
if not item.has_key('alt'):
|
||||
item['alt'] = 'image'
|
||||
return soup
|
||||
|
|
|
|||
120
resources/recipes/roger_ebert.recipe
Normal file
120
resources/recipes/roger_ebert.recipe
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import re
|
||||
import urllib2
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, SoupStrainer
|
||||
|
||||
class Ebert(BasicNewsRecipe):
|
||||
title = 'Roger Ebert'
|
||||
__author__ = 'Shane Erstad'
|
||||
description = 'Roger Ebert Movie Reviews'
|
||||
publisher = 'Chicago Sun Times'
|
||||
category = 'movies'
|
||||
oldest_article = 8
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
encoding = 'utf-8'
|
||||
masthead_url = 'http://rogerebert.suntimes.com/graphics/global/roger.jpg'
|
||||
language = 'en'
|
||||
remove_empty_feeds = False
|
||||
PREFIX = 'http://rogerebert.suntimes.com'
|
||||
patternReviews = r'<span class="*?movietitle"*?>(.*?)</span>.*?<div class="*?headline"*?>(.*?)</div>(.*?)</div>'
|
||||
patternCommentary = r'<div class="*?headline"*?>.*?(<a href="/apps/pbcs.dll/article\?AID=.*?COMMENTARY.*?" id="ltred">.*?</a>).*?<div class="blurb clear">(.*?)</div>'
|
||||
patternPeople = r'<div class="*?headline"*?>.*?(<a href="/apps/pbcs.dll/article\?AID=.*?PEOPLE.*?" id="ltred">.*?</a>).*?<div class="blurb clear">(.*?)</div>'
|
||||
patternGlossary = r'<div class="*?headline"*?>.*?(<a href="/apps/pbcs.dll/article\?AID=.*?GLOSSARY.*?" id="ltred">.*?</a>).*?<div class="blurb clear">(.*?)</div>'
|
||||
|
||||
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
, 'linearize_tables' : True
|
||||
}
|
||||
|
||||
|
||||
feeds = [
|
||||
(u'Reviews' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=reviews' )
|
||||
,(u'Commentary' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=COMMENTARY')
|
||||
,(u'Great Movies' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=REVIEWS08')
|
||||
,(u'People' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=PEOPLE')
|
||||
,(u'Glossary' , u'http://rogerebert.suntimes.com/apps/pbcs.dll/section?category=GLOSSARY')
|
||||
|
||||
]
|
||||
|
||||
preprocess_regexps = [
|
||||
(re.compile(r'<font.*?>.*?This is a printer friendly.*?</font>.*?<hr>', re.DOTALL|re.IGNORECASE),
|
||||
lambda m: '')
|
||||
]
|
||||
|
||||
|
||||
|
||||
def print_version(self, url):
|
||||
return url + '&template=printart'
|
||||
|
||||
def parse_index(self):
|
||||
totalfeeds = []
|
||||
lfeeds = self.get_feeds()
|
||||
for feedobj in lfeeds:
|
||||
feedtitle, feedurl = feedobj
|
||||
self.log('\tFeedurl: ', feedurl)
|
||||
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
|
||||
articles = []
|
||||
page = urllib2.urlopen(feedurl).read()
|
||||
|
||||
if feedtitle == 'Reviews' or feedtitle == 'Great Movies':
|
||||
pattern = self.patternReviews
|
||||
elif feedtitle == 'Commentary':
|
||||
pattern = self.patternCommentary
|
||||
elif feedtitle == 'People':
|
||||
pattern = self.patternPeople
|
||||
elif feedtitle == 'Glossary':
|
||||
pattern = self.patternGlossary
|
||||
|
||||
|
||||
regex = re.compile(pattern, re.IGNORECASE|re.DOTALL)
|
||||
|
||||
for match in regex.finditer(page):
|
||||
if feedtitle == 'Reviews' or feedtitle == 'Great Movies':
|
||||
movietitle = match.group(1)
|
||||
thislink = match.group(2)
|
||||
description = match.group(3)
|
||||
elif feedtitle == 'Commentary' or feedtitle == 'People' or feedtitle == 'Glossary':
|
||||
thislink = match.group(1)
|
||||
description = match.group(2)
|
||||
|
||||
self.log(thislink)
|
||||
|
||||
for link in BeautifulSoup(thislink, parseOnlyThese=SoupStrainer('a')):
|
||||
thisurl = self.PREFIX + link['href']
|
||||
thislinktext = self.tag_to_string(link)
|
||||
|
||||
if feedtitle == 'Reviews' or feedtitle == 'Great Movies':
|
||||
thistitle = movietitle
|
||||
elif feedtitle == 'Commentary' or feedtitle == 'People' or feedtitle == 'Glossary':
|
||||
thistitle = thislinktext
|
||||
|
||||
if thistitle == '':
|
||||
thistitle = 'Ebert Journal Post'
|
||||
|
||||
"""
|
||||
pattern2 = r'AID=\/(.*?)\/'
|
||||
reg2 = re.compile(pattern2, re.IGNORECASE|re.DOTALL)
|
||||
match2 = reg2.search(thisurl)
|
||||
date = match2.group(1)
|
||||
c = time.strptime(match2.group(1),"%Y%m%d")
|
||||
date=time.strftime("%a, %b %d, %Y", c)
|
||||
self.log(date)
|
||||
"""
|
||||
|
||||
articles.append({
|
||||
'title' :thistitle
|
||||
,'date' :''
|
||||
,'url' :thisurl
|
||||
,'description':description
|
||||
})
|
||||
totalfeeds.append((feedtitle, articles))
|
||||
|
||||
return totalfeeds
|
||||
|
||||
|
|
@ -43,8 +43,9 @@ class Stage3(Command):
|
|||
|
||||
description = 'Stage 3 of the publish process'
|
||||
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist',
|
||||
'upload_to_google_code', 'tag_release', 'upload_to_server',
|
||||
'upload_to_sourceforge', 'upload_to_mobileread',
|
||||
'upload_to_google_code', 'upload_to_sourceforge',
|
||||
'tag_release', 'upload_to_server',
|
||||
'upload_to_mobileread',
|
||||
]
|
||||
|
||||
class Stage4(Command):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.40'
|
||||
__version__ = '0.7.42'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ class Plumber(object):
|
|||
]
|
||||
|
||||
def __init__(self, input, output, log, report_progress=DummyReporter(),
|
||||
dummy=False, merge_plugin_recs=True, abort_after_input_dump=False):
|
||||
dummy=False, merge_plugin_recs=True, abort_after_input_dump=False,
|
||||
override_input_metadata=False):
|
||||
'''
|
||||
:param input: Path to input file.
|
||||
:param output: Path to output file/directory
|
||||
|
|
@ -87,6 +88,7 @@ def __init__(self, input, output, log, report_progress=DummyReporter(),
|
|||
self.log = log
|
||||
self.ui_reporter = report_progress
|
||||
self.abort_after_input_dump = abort_after_input_dump
|
||||
self.override_input_metadata = override_input_metadata
|
||||
|
||||
# Pipeline options {{{
|
||||
# Initialize the conversion options that are independent of input and
|
||||
|
|
@ -924,7 +926,8 @@ def run(self):
|
|||
self.opts.dest = self.opts.output_profile
|
||||
|
||||
from calibre.ebooks.oeb.transforms.metadata import MergeMetadata
|
||||
MergeMetadata()(self.oeb, self.user_metadata, self.opts)
|
||||
MergeMetadata()(self.oeb, self.user_metadata, self.opts,
|
||||
override_input_metadata=self.override_input_metadata)
|
||||
pr(0.2)
|
||||
self.flush()
|
||||
|
||||
|
|
|
|||
|
|
@ -137,17 +137,17 @@ def markup_italicis(self, html):
|
|||
]
|
||||
|
||||
ITALICIZE_STYLE_PATS = [
|
||||
r'(?msu)(?<=\s)_(?P<words>\S[^_]{0,40}?\S)?_(?=\s)',
|
||||
r'(?msu)(?<=\s)/(?P<words>\S[^/]{0,40}?\S)?/(?=\s)',
|
||||
r'(?msu)(?<=\s)~~(?P<words>\S[^~]{0,40}?\S)?~~(?=\s)',
|
||||
r'(?msu)(?<=\s)\*(?P<words>\S[^\*]{0,40}?\S)?\*(?=\s)',
|
||||
r'(?msu)(?<=\s)~(?P<words>\S[^~]{0,40}?\S)?~(?=\s)',
|
||||
r'(?msu)(?<=\s)_/(?P<words>\S[^/_]{0,40}?\S)?/_(?=\s)',
|
||||
r'(?msu)(?<=\s)_\*(?P<words>\S[^\*_]{0,40}?\S)?\*_(?=\s)',
|
||||
r'(?msu)(?<=\s)\*/(?P<words>\S[^/\*]{0,40}?\S)?/\*(?=\s)',
|
||||
r'(?msu)(?<=\s)_\*/(?P<words>\S[^\*_]{0,40}?\S)?/\*_(?=\s)',
|
||||
r'(?msu)(?<=\s)/:(?P<words>\S[^:/]{0,40}?\S)?:/(?=\s)',
|
||||
r'(?msu)(?<=\s)\|:(?P<words>\S[^:\|]{0,40}?\S)?:\|(?=\s)',
|
||||
r'(?msu)(?<=\s)_(?P<words>\S[^_]{0,40}?\S)?_(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)/(?P<words>\S[^/]{0,40}?\S)?/(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)~~(?P<words>\S[^~]{0,40}?\S)?~~(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)\*(?P<words>\S[^\*]{0,40}?\S)?\*(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)~(?P<words>\S[^~]{0,40}?\S)?~(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)_/(?P<words>\S[^/_]{0,40}?\S)?/_(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)_\*(?P<words>\S[^\*_]{0,40}?\S)?\*_(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)\*/(?P<words>\S[^/\*]{0,40}?\S)?/\*(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)_\*/(?P<words>\S[^\*_]{0,40}?\S)?/\*_(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)/:(?P<words>\S[^:/]{0,40}?\S)?:/(?=[\s\.,\!\?])',
|
||||
r'(?msu)(?<=\s)\|:(?P<words>\S[^:\|]{0,40}?\S)?:\|(?=[\s\.,\!\?])',
|
||||
]
|
||||
|
||||
for word in ITALICIZE_WORDS:
|
||||
|
|
|
|||
|
|
@ -1541,7 +1541,10 @@ def _build_exth(self):
|
|||
exth.write(data)
|
||||
nrecs += 1
|
||||
if term == 'rights' :
|
||||
rights = unicode(oeb.metadata.rights[0]).encode('utf-8')
|
||||
try:
|
||||
rights = unicode(oeb.metadata.rights[0]).encode('utf-8')
|
||||
except:
|
||||
rights = 'Unknown'
|
||||
exth.write(pack('>II', EXTH_CODES['rights'], len(rights) + 8))
|
||||
exth.write(rights)
|
||||
|
||||
|
|
|
|||
|
|
@ -221,7 +221,10 @@ def set_property(v):
|
|||
el.text):
|
||||
stylesheet = parseString(el.text)
|
||||
replaceUrls(stylesheet, link_repl_func)
|
||||
el.text = '\n'+stylesheet.cssText + '\n'
|
||||
repl = stylesheet.cssText
|
||||
if isbytestring(repl):
|
||||
repl = repl.decode('utf-8')
|
||||
el.text = '\n'+ repl + '\n'
|
||||
|
||||
if 'style' in el.attrib:
|
||||
text = el.attrib['style']
|
||||
|
|
@ -234,8 +237,11 @@ def set_property(v):
|
|||
set_property(item)
|
||||
elif v.CSS_PRIMITIVE_VALUE == v.cssValueType:
|
||||
set_property(v)
|
||||
el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r',
|
||||
repl = stext.cssText.replace('\n', ' ').replace('\r',
|
||||
' ')
|
||||
if isbytestring(repl):
|
||||
repl = repl.decode('utf-8')
|
||||
el.attrib['style'] = repl
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
from calibre.utils.date import isoformat, now
|
||||
from calibre import guess_type
|
||||
|
||||
def meta_info_to_oeb_metadata(mi, m, log):
|
||||
def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
|
||||
from calibre.ebooks.oeb.base import OPF
|
||||
if not mi.is_null('title'):
|
||||
m.clear('title')
|
||||
|
|
@ -29,15 +29,23 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
|||
if not mi.is_null('book_producer'):
|
||||
m.filter('contributor', lambda x : x.role.lower() == 'bkp')
|
||||
m.add('contributor', mi.book_producer, role='bkp')
|
||||
elif override_input_metadata:
|
||||
m.filter('contributor', lambda x : x.role.lower() == 'bkp')
|
||||
if not mi.is_null('comments'):
|
||||
m.clear('description')
|
||||
m.add('description', mi.comments)
|
||||
elif override_input_metadata:
|
||||
m.clear('description')
|
||||
if not mi.is_null('publisher'):
|
||||
m.clear('publisher')
|
||||
m.add('publisher', mi.publisher)
|
||||
elif override_input_metadata:
|
||||
m.clear('publisher')
|
||||
if not mi.is_null('series'):
|
||||
m.clear('series')
|
||||
m.add('series', mi.series)
|
||||
elif override_input_metadata:
|
||||
m.clear('series')
|
||||
if not mi.is_null('isbn'):
|
||||
has = False
|
||||
for x in m.identifier:
|
||||
|
|
@ -46,19 +54,27 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
|||
has = True
|
||||
if not has:
|
||||
m.add('identifier', mi.isbn, scheme='ISBN')
|
||||
elif override_input_metadata:
|
||||
m.filter('identifier', lambda x: x.scheme.lower() == 'isbn')
|
||||
if not mi.is_null('language'):
|
||||
m.clear('language')
|
||||
m.add('language', mi.language)
|
||||
if not mi.is_null('series_index'):
|
||||
m.clear('series_index')
|
||||
m.add('series_index', mi.format_series_index())
|
||||
elif override_input_metadata:
|
||||
m.clear('series_index')
|
||||
if not mi.is_null('rating'):
|
||||
m.clear('rating')
|
||||
m.add('rating', '%.2f'%mi.rating)
|
||||
elif override_input_metadata:
|
||||
m.clear('rating')
|
||||
if not mi.is_null('tags'):
|
||||
m.clear('subject')
|
||||
for t in mi.tags:
|
||||
m.add('subject', t)
|
||||
elif override_input_metadata:
|
||||
m.clear('subject')
|
||||
if not mi.is_null('pubdate'):
|
||||
m.clear('date')
|
||||
m.add('date', isoformat(mi.pubdate))
|
||||
|
|
@ -71,6 +87,7 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
|||
if not mi.is_null('publication_type'):
|
||||
m.clear('publication_type')
|
||||
m.add('publication_type', mi.publication_type)
|
||||
|
||||
if not m.timestamp:
|
||||
m.add('timestamp', isoformat(now()))
|
||||
|
||||
|
|
@ -78,11 +95,12 @@ def meta_info_to_oeb_metadata(mi, m, log):
|
|||
class MergeMetadata(object):
|
||||
'Merge in user metadata, including cover'
|
||||
|
||||
def __call__(self, oeb, mi, opts):
|
||||
def __call__(self, oeb, mi, opts, override_input_metadata=False):
|
||||
self.oeb, self.log = oeb, oeb.log
|
||||
m = self.oeb.metadata
|
||||
self.log('Merging user specified metadata...')
|
||||
meta_info_to_oeb_metadata(mi, m, oeb.log)
|
||||
meta_info_to_oeb_metadata(mi, m, oeb.log,
|
||||
override_input_metadata=override_input_metadata)
|
||||
cover_id = self.set_cover(mi, opts.prefer_metadata_cover)
|
||||
m.clear('cover')
|
||||
if cover_id is not None:
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ def dump_text(self, elem, stylizer, tag_stack=[]):
|
|||
|
||||
if hasattr(elem, 'tail') and elem.tail != None and elem.tail.strip() != '':
|
||||
if 'block' in tag_stack:
|
||||
text += '%s ' % txt2rtf(elem.tail)
|
||||
text += '%s' % txt2rtf(elem.tail)
|
||||
else:
|
||||
text += '{\\par \\pard \\hyphpar %s}' % txt2rtf(elem.tail)
|
||||
|
||||
|
|
|
|||
|
|
@ -384,7 +384,28 @@ def switch_requested(self, location):
|
|||
return
|
||||
|
||||
prefs['library_path'] = loc
|
||||
#from calibre.utils.mem import memory
|
||||
#import weakref
|
||||
#from PyQt4.Qt import QTimer
|
||||
#self.dbref = weakref.ref(self.gui.library_view.model().db)
|
||||
#self.before_mem = memory()/1024**2
|
||||
self.gui.library_moved(loc)
|
||||
#QTimer.singleShot(1000, self.debug_leak)
|
||||
|
||||
def debug_leak(self):
|
||||
import gc
|
||||
from calibre.utils.mem import memory
|
||||
ref = self.dbref
|
||||
for i in xrange(3): gc.collect()
|
||||
if ref() is not None:
|
||||
print 11111, ref()
|
||||
for r in gc.get_referrers(ref())[:10]:
|
||||
print r
|
||||
print
|
||||
print 'before:', self.before_mem
|
||||
print 'after:', memory()/1024**2
|
||||
self.dbref = self.before_mem = None
|
||||
|
||||
|
||||
def qs_requested(self, idx, *args):
|
||||
self.switch_requested(self.qs_locations[idx])
|
||||
|
|
|
|||
|
|
@ -144,6 +144,9 @@ def initialize(self, name, db):
|
|||
# Hook changes to thumb_width
|
||||
self.thumb_width.valueChanged.connect(self.thumb_width_changed)
|
||||
|
||||
# Hook changes to Description section
|
||||
self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed)
|
||||
|
||||
def options(self):
|
||||
# Save/return the current options
|
||||
# exclude_genre stores literally
|
||||
|
|
@ -265,7 +268,7 @@ def populateComboBoxes(self):
|
|||
custom_fields = {}
|
||||
for custom_field in all_custom_fields:
|
||||
field_md = self.db.metadata_for_field(custom_field)
|
||||
if field_md['datatype'] in ['text','comments']:
|
||||
if field_md['datatype'] in ['text','comments','composite']:
|
||||
custom_fields[field_md['name']] = {'field':custom_field,
|
||||
'datatype':field_md['datatype']}
|
||||
# Blank field first
|
||||
|
|
@ -324,6 +327,28 @@ def exclude_source_field_changed(self,new_index):
|
|||
else:
|
||||
self.exclude_pattern.setEnabled(False)
|
||||
|
||||
def generate_descriptions_changed(self,new_state):
|
||||
'''
|
||||
Process changes to Descriptions section
|
||||
0: unchecked
|
||||
2: checked
|
||||
'''
|
||||
|
||||
return
|
||||
|
||||
if new_state == 0:
|
||||
# unchecked
|
||||
self.merge_source_field.setEnabled(False)
|
||||
self.merge_before.setEnabled(False)
|
||||
self.merge_after.setEnabled(False)
|
||||
self.include_hr.setEnabled(False)
|
||||
elif new_state == 2:
|
||||
# checked
|
||||
self.merge_source_field.setEnabled(True)
|
||||
self.merge_before.setEnabled(True)
|
||||
self.merge_after.setEnabled(True)
|
||||
self.include_hr.setEnabled(True)
|
||||
|
||||
def header_note_source_field_changed(self,new_index):
|
||||
'''
|
||||
Process changes in the header_note_source_field combo box
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
|
||||
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
|
||||
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
|
||||
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog
|
||||
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog, \
|
||||
QHBoxLayout
|
||||
from PyQt4.QtWebKit import QWebView, QWebPage
|
||||
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
|
|
@ -488,7 +489,7 @@ def highlightBlock(self, text):
|
|||
|
||||
class Editor(QWidget): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent=None, one_line_toolbar=False):
|
||||
QWidget.__init__(self, parent)
|
||||
self.toolbar1 = QToolBar(self)
|
||||
self.toolbar2 = QToolBar(self)
|
||||
|
|
@ -508,9 +509,14 @@ def __init__(self, parent=None):
|
|||
self.wyswyg.layout = l = QVBoxLayout(self.wyswyg)
|
||||
self.setLayout(self._layout)
|
||||
l.setContentsMargins(0, 0, 0, 0)
|
||||
l.addWidget(self.toolbar1)
|
||||
l.addWidget(self.toolbar2)
|
||||
l.addWidget(self.toolbar3)
|
||||
if one_line_toolbar:
|
||||
tb = QHBoxLayout()
|
||||
l.addLayout(tb)
|
||||
else:
|
||||
tb = l
|
||||
tb.addWidget(self.toolbar1)
|
||||
tb.addWidget(self.toolbar2)
|
||||
tb.addWidget(self.toolbar3)
|
||||
l.addWidget(self.editor)
|
||||
self._layout.addWidget(self.tabs)
|
||||
self.tabs.addTab(self.wyswyg, _('Normal view'))
|
||||
|
|
|
|||
|
|
@ -12,17 +12,24 @@
|
|||
from calibre.utils.logging import Log
|
||||
|
||||
def gui_convert(input, output, recommendations, notification=DummyReporter(),
|
||||
abort_after_input_dump=False, log=None):
|
||||
abort_after_input_dump=False, log=None, override_input_metadata=False):
|
||||
recommendations = list(recommendations)
|
||||
recommendations.append(('verbose', 2, OptionRecommendation.HIGH))
|
||||
if log is None:
|
||||
log = Log()
|
||||
plumber = Plumber(input, output, log, report_progress=notification,
|
||||
abort_after_input_dump=abort_after_input_dump)
|
||||
abort_after_input_dump=abort_after_input_dump,
|
||||
override_input_metadata=override_input_metadata)
|
||||
plumber.merge_ui_recommendations(recommendations)
|
||||
|
||||
plumber.run()
|
||||
|
||||
def gui_convert_override(input, output, recommendations, notification=DummyReporter(),
|
||||
abort_after_input_dump=False, log=None):
|
||||
gui_convert(input, output, recommendations, notification=notification,
|
||||
abort_after_input_dump=abort_after_input_dump, log=log,
|
||||
override_input_metadata=True)
|
||||
|
||||
def gui_catalog(fmt, title, dbspec, ids, out_file_name, sync, fmt_options, connected_device,
|
||||
notification=DummyReporter(), log=None):
|
||||
if log is None:
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ def __init__(self, db, book_id, regex, *args):
|
|||
self.connect(self.button_box, SIGNAL('clicked(QAbstractButton*)'), self.button_clicked)
|
||||
self.connect(self.regex, SIGNAL('textChanged(QString)'), self.regex_valid)
|
||||
self.connect(self.test, SIGNAL('clicked()'), self.do_test)
|
||||
self.connect(self.previous, SIGNAL('clicked()'), self.goto_previous)
|
||||
self.connect(self.next, SIGNAL('clicked()'), self.goto_next)
|
||||
|
||||
self.match_locs = []
|
||||
|
||||
def regex_valid(self):
|
||||
regex = unicode(self.regex.text())
|
||||
|
|
@ -42,15 +46,23 @@ def regex_valid(self):
|
|||
try:
|
||||
re.compile(regex)
|
||||
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgba(0,255,0,20%); }')
|
||||
return True
|
||||
except:
|
||||
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgb(255,0,0,20%); }')
|
||||
return False
|
||||
else:
|
||||
self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }')
|
||||
return True
|
||||
self.preview.setExtraSelections([])
|
||||
|
||||
self.match_locs = []
|
||||
self.next.setEnabled(False)
|
||||
self.previous.setEnabled(False)
|
||||
self.occurrences.setText('0')
|
||||
|
||||
return False
|
||||
|
||||
def do_test(self):
|
||||
selections = []
|
||||
self.match_locs = []
|
||||
if self.regex_valid():
|
||||
text = unicode(self.preview.toPlainText())
|
||||
regex = unicode(self.regex.text())
|
||||
|
|
@ -64,9 +76,43 @@ def do_test(self):
|
|||
es.cursor.setPosition(match.start(), QTextCursor.MoveAnchor)
|
||||
es.cursor.setPosition(match.end(), QTextCursor.KeepAnchor)
|
||||
selections.append(es)
|
||||
self.match_locs.append((match.start(), match.end()))
|
||||
except:
|
||||
pass
|
||||
self.preview.setExtraSelections(selections)
|
||||
if self.match_locs:
|
||||
self.next.setEnabled(True)
|
||||
self.previous.setEnabled(True)
|
||||
self.occurrences.setText(str(len(self.match_locs)))
|
||||
|
||||
def goto_previous(self):
|
||||
pos = self.preview.textCursor().position()
|
||||
if self.match_locs:
|
||||
match_loc = len(self.match_locs) - 1
|
||||
for i in xrange(len(self.match_locs) - 1, -1, -1):
|
||||
loc = self.match_locs[i][1]
|
||||
if pos > loc:
|
||||
match_loc = i
|
||||
break
|
||||
self.goto_loc(self.match_locs[match_loc][1], operation=QTextCursor.Left, n=self.match_locs[match_loc][1] - self.match_locs[match_loc][0])
|
||||
|
||||
def goto_next(self):
|
||||
pos = self.preview.textCursor().position()
|
||||
if self.match_locs:
|
||||
match_loc = 0
|
||||
for i in xrange(len(self.match_locs)):
|
||||
loc = self.match_locs[i][0]
|
||||
if pos < loc:
|
||||
match_loc = i
|
||||
break
|
||||
self.goto_loc(self.match_locs[match_loc][0], n=self.match_locs[match_loc][1] - self.match_locs[match_loc][0])
|
||||
|
||||
def goto_loc(self, loc, operation=QTextCursor.Right, mode=QTextCursor.KeepAnchor, n=0):
|
||||
cursor = QTextCursor(self.preview.document())
|
||||
cursor.setPosition(loc)
|
||||
if n:
|
||||
cursor.movePosition(operation, mode, n)
|
||||
self.preview.setTextCursor(cursor)
|
||||
|
||||
def select_format(self, db, book_id):
|
||||
format = None
|
||||
|
|
@ -125,6 +171,11 @@ def builder(self):
|
|||
if bld.exec_() == bld.Accepted:
|
||||
self.edit.setText(bld.regex.text())
|
||||
|
||||
def setObjectName(self, *args):
|
||||
QWidget.setObjectName(self, *args)
|
||||
if hasattr(self, 'edit'):
|
||||
self.edit.initialize('regex_edit_'+unicode(self.objectName()))
|
||||
|
||||
def set_msg(self, msg):
|
||||
self.msg.setText(msg)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,102 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>662</width>
|
||||
<height>505</height>
|
||||
<width>580</width>
|
||||
<height>503</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Regex Builder</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0" colspan="5">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Regex:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="regex"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="test">
|
||||
<property name="text">
|
||||
<string>Test</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Occurrences:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="occurrences">
|
||||
<property name="text">
|
||||
<string>0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>298</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Goto:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="previous">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Previous</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="next">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Next</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Preview</string>
|
||||
|
|
@ -36,32 +123,28 @@
|
|||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="4">
|
||||
<widget class="QDialogButtonBox" name="button_box">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Regex:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="4">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="regex"/>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>328</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="test">
|
||||
<property name="text">
|
||||
<string>Test</string>
|
||||
<widget class="QDialogButtonBox" name="button_box">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
device_plugins
|
||||
from calibre.devices.interface import DevicePlugin
|
||||
from calibre.devices.errors import UserFeedback, OpenFeedback
|
||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||
from calibre.gui2.dialogs.choose_format_device import ChooseFormatDeviceDialog
|
||||
from calibre.utils.ipc.job import BaseJob
|
||||
from calibre.devices.scanner import DeviceScanner
|
||||
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
||||
|
|
@ -826,8 +826,24 @@ def dispatch_sync_event(self, dest, delete, specific):
|
|||
|
||||
fmt = None
|
||||
if specific:
|
||||
d = ChooseFormatDialog(self, _('Choose format to send to device'),
|
||||
self.device_manager.device.settings().format_map)
|
||||
formats = []
|
||||
aval_out_formats = available_output_formats()
|
||||
format_count = {}
|
||||
for row in rows:
|
||||
fmts = self.library_view.model().db.formats(row.row())
|
||||
if fmts:
|
||||
for f in fmts.split(','):
|
||||
f = f.lower()
|
||||
if format_count.has_key(f):
|
||||
format_count[f] += 1
|
||||
else:
|
||||
format_count[f] = 1
|
||||
for f in self.device_manager.device.settings().format_map:
|
||||
if f in format_count.keys():
|
||||
formats.append((f, _('%i of %i Books' % (format_count[f], len(rows))), True if f in aval_out_formats else False))
|
||||
elif f in aval_out_formats:
|
||||
formats.append((f, _('0 of %i Books' % len(rows)), True))
|
||||
d = ChooseFormatDeviceDialog(self, _('Choose format to send to device'), formats)
|
||||
if d.exec_() != QDialog.Accepted:
|
||||
return
|
||||
if d.format():
|
||||
|
|
|
|||
53
src/calibre/gui2/dialogs/choose_format_device.py
Normal file
53
src/calibre/gui2/dialogs/choose_format_device.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
|
||||
from PyQt4.Qt import QDialog, QTreeWidgetItem, QIcon, SIGNAL
|
||||
|
||||
from calibre.gui2 import file_icon_provider
|
||||
from calibre.gui2.dialogs.choose_format_device_ui import Ui_ChooseFormatDeviceDialog
|
||||
|
||||
class ChooseFormatDeviceDialog(QDialog, Ui_ChooseFormatDeviceDialog):
|
||||
|
||||
def __init__(self, window, msg, formats):
|
||||
'''
|
||||
formats is a list of tuples: [(format, exists, convertible)].
|
||||
format: Lower case format identifier. E.G. mobi
|
||||
exists: String representing the number of books that
|
||||
exist in the format.
|
||||
convertible: True if the format is a convertible format.
|
||||
formats should be ordered in the device's preferred format ordering.
|
||||
'''
|
||||
QDialog.__init__(self, window)
|
||||
Ui_ChooseFormatDeviceDialog.__init__(self)
|
||||
self.setupUi(self)
|
||||
self.connect(self.formats, SIGNAL('activated(QModelIndex)'),
|
||||
self.activated_slot)
|
||||
|
||||
self.msg.setText(msg)
|
||||
for i, (format, exists, convertible) in enumerate(formats):
|
||||
t_item = QTreeWidgetItem()
|
||||
t_item.setIcon(0, file_icon_provider().icon_from_ext(format.lower()))
|
||||
t_item.setText(0, format.upper())
|
||||
t_item.setText(1, exists)
|
||||
if convertible:
|
||||
t_item.setIcon(2, QIcon(I('ok.png')))
|
||||
self.formats.addTopLevelItem(t_item)
|
||||
if i == 0:
|
||||
self.formats.setCurrentItem(t_item)
|
||||
t_item.setSelected(True)
|
||||
self.formats.resizeColumnToContents(2)
|
||||
self.formats.resizeColumnToContents(1)
|
||||
self.formats.resizeColumnToContents(0)
|
||||
self.formats.header().resizeSection(0, self.formats.header().sectionSize(0) * 2)
|
||||
self._format = None
|
||||
|
||||
def activated_slot(self, *args):
|
||||
self.accept()
|
||||
|
||||
def format(self):
|
||||
return self._format
|
||||
|
||||
def accept(self):
|
||||
self._format = unicode(self.formats.currentItem().text(0))
|
||||
return QDialog.accept(self)
|
||||
|
||||
111
src/calibre/gui2/dialogs/choose_format_device.ui
Normal file
111
src/calibre/gui2/dialogs/choose_format_device.ui
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ChooseFormatDeviceDialog</class>
|
||||
<widget class="QDialog" name="ChooseFormatDeviceDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>507</width>
|
||||
<height>377</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Choose Format</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/mimetypes/unknown.png</normaloff>:/images/mimetypes/unknown.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="msg">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="formats">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="allColumnsShowFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Format</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Existing</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignLeft|AlignVCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Convertible</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>ChooseFormatDeviceDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>ChooseFormatDeviceDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
|
|
@ -6,8 +6,7 @@
|
|||
import re, os
|
||||
|
||||
from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \
|
||||
pyqtSignal, QDialogButtonBox
|
||||
from PyQt4 import QtGui
|
||||
pyqtSignal, QDialogButtonBox, QDate, QLineEdit
|
||||
|
||||
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||
|
|
@ -302,6 +301,7 @@ def __init__(self, window, rows, model, tab):
|
|||
self.pubdate.setSpecialValueText(_('Undefined'))
|
||||
self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
|
||||
self.pubdate.dateChanged.connect(self.do_apply_pubdate)
|
||||
self.adddate.setDate(QDate.currentDate())
|
||||
self.adddate.setMinimumDate(UNDEFINED_QDATE)
|
||||
self.adddate.setSpecialValueText(_('Undefined'))
|
||||
self.clear_adddate_button.clicked.connect(self.clear_adddate)
|
||||
|
|
@ -365,16 +365,16 @@ def prepare_search_and_replace(self):
|
|||
offset = 10
|
||||
self.s_r_number_of_books = min(10, len(self.ids))
|
||||
for i in range(1,self.s_r_number_of_books+1):
|
||||
w = QtGui.QLabel(self.tabWidgetPage3)
|
||||
w = QLabel(self.tabWidgetPage3)
|
||||
w.setText(_('Book %d:')%i)
|
||||
self.testgrid.addWidget(w, i+offset, 0, 1, 1)
|
||||
w = QtGui.QLineEdit(self.tabWidgetPage3)
|
||||
w = QLineEdit(self.tabWidgetPage3)
|
||||
w.setReadOnly(True)
|
||||
name = 'book_%d_text'%i
|
||||
setattr(self, name, w)
|
||||
self.book_1_text.setObjectName(name)
|
||||
self.testgrid.addWidget(w, i+offset, 1, 1, 1)
|
||||
w = QtGui.QLineEdit(self.tabWidgetPage3)
|
||||
w = QLineEdit(self.tabWidgetPage3)
|
||||
w.setReadOnly(True)
|
||||
name = 'book_%d_result'%i
|
||||
setattr(self, name, w)
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ def delete_tags(self, item=None):
|
|||
|
||||
def apply_tags(self, item=None):
|
||||
items = self.available_tags.selectedItems() if item is None else [item]
|
||||
rows = [self.available_tags.row(i) for i in items]
|
||||
row = max(rows)
|
||||
for item in items:
|
||||
tag = unicode(item.text())
|
||||
self.tags.append(tag)
|
||||
|
|
@ -89,6 +91,12 @@ def apply_tags(self, item=None):
|
|||
for tag in self.tags:
|
||||
self.applied_tags.addItem(tag)
|
||||
|
||||
if row >= self.available_tags.count():
|
||||
row = self.available_tags.count() - 1
|
||||
|
||||
if row > 2:
|
||||
item = self.available_tags.item(row)
|
||||
self.available_tags.scrollToItem(item)
|
||||
|
||||
|
||||
def unapply_tags(self, item=None):
|
||||
|
|
|
|||
|
|
@ -356,6 +356,13 @@ def clear(self):
|
|||
self.populate_options(AutomaticNewsRecipe)
|
||||
self.source_code.setText('')
|
||||
|
||||
def reject(self):
|
||||
if question_dialog(self, _('Are you sure?'),
|
||||
_('You will lose any unsaved changes. To save your'
|
||||
' changes, click the Add/Update recipe button.'
|
||||
' Continue?'), show_copy_button=False):
|
||||
ResizableDialog.reject(self)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
is_ok_to_use_qt()
|
||||
|
|
|
|||
|
|
@ -150,13 +150,13 @@ def __init__(self, opts, args, actions, listener, app, gui_debug=None):
|
|||
if DEBUG:
|
||||
prints('Starting up...')
|
||||
|
||||
def start_gui(self):
|
||||
def start_gui(self, db):
|
||||
from calibre.gui2.ui import Main
|
||||
main = Main(self.opts, gui_debug=self.gui_debug)
|
||||
if self.splash_screen is not None:
|
||||
self.splash_screen.showMessage(_('Initializing user interface...'))
|
||||
self.splash_screen.finish(main)
|
||||
main.initialize(self.library_path, self.db, self.listener, self.actions)
|
||||
main.initialize(self.library_path, db, self.listener, self.actions)
|
||||
if DEBUG:
|
||||
prints('Started up in', time.time() - self.startup_time)
|
||||
add_filesystem_book = partial(main.iactions['Add Books'].add_filesystem_book, allow_device=False)
|
||||
|
|
@ -200,8 +200,7 @@ def initialize_db_stage2(self, db, tb):
|
|||
det_msg=traceback.format_exc(), show=True)
|
||||
self.initialization_failed()
|
||||
|
||||
self.db = db
|
||||
self.start_gui()
|
||||
self.start_gui(db)
|
||||
|
||||
def initialize_db(self):
|
||||
db = None
|
||||
|
|
|
|||
|
|
@ -77,9 +77,9 @@ def initialize(self, db, id_):
|
|||
def commit(self, db, id_):
|
||||
title = self.current_val
|
||||
if self.COMMIT:
|
||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False)
|
||||
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False)
|
||||
else:
|
||||
getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False,
|
||||
getattr(db, 'set_'+ self.TITLE_ATTR)(id_, title, notify=False,
|
||||
commit=False)
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@
|
|||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
|
||||
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
|
||||
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
|
||||
QSizePolicy
|
||||
QSizePolicy, QPalette, QFrame, QSize
|
||||
|
||||
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
||||
from calibre.gui2 import ResizableDialog, error_dialog, gprefs
|
||||
|
|
@ -21,12 +22,15 @@
|
|||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||
from calibre.utils.config import tweaks
|
||||
|
||||
class MetadataSingleDialog(ResizableDialog):
|
||||
class MetadataSingleDialogBase(ResizableDialog):
|
||||
|
||||
view_format = pyqtSignal(object)
|
||||
cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields']
|
||||
one_line_comments_toolbar = False
|
||||
|
||||
def __init__(self, db, parent=None):
|
||||
self.db = db
|
||||
self.changed = set([])
|
||||
ResizableDialog.__init__(self, parent)
|
||||
|
||||
def setupUi(self, *args): # {{{
|
||||
|
|
@ -37,6 +41,14 @@ def setupUi(self, *args): # {{{
|
|||
self)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
|
||||
self)
|
||||
self.next_button.clicked.connect(partial(self.do_one, delta=1))
|
||||
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
|
||||
self)
|
||||
self.button_box.addButton(self.prev_button, self.button_box.ActionRole)
|
||||
self.button_box.addButton(self.next_button, self.button_box.ActionRole)
|
||||
self.prev_button.clicked.connect(partial(self.do_one, delta=-1))
|
||||
|
||||
self.scroll_area = QScrollArea(self)
|
||||
self.scroll_area.setFrameShape(QScrollArea.NoFrame)
|
||||
|
|
@ -51,13 +63,11 @@ def setupUi(self, *args): # {{{
|
|||
self.l.addWidget(self.button_box)
|
||||
|
||||
self.setWindowIcon(QIcon(I('edit_input.png')))
|
||||
self.setWindowTitle(_('Edit Meta Information'))
|
||||
self.setWindowTitle(_('Edit Metadata'))
|
||||
|
||||
self.create_basic_metadata_widgets()
|
||||
|
||||
if len(self.db.custom_column_label_map) == 0:
|
||||
self.central_widget.tabBar().setVisible(False)
|
||||
else:
|
||||
if len(self.db.custom_column_label_map):
|
||||
self.create_custom_metadata_widgets()
|
||||
|
||||
|
||||
|
|
@ -91,7 +101,7 @@ def create_basic_metadata_widgets(self): # {{{
|
|||
'Using this button to create author sort will change author sort from'
|
||||
' red to green.'))
|
||||
self.author_sort = AuthorSortEdit(self, self.authors,
|
||||
self.deduce_author_sort_button, db)
|
||||
self.deduce_author_sort_button, self.db)
|
||||
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
||||
|
||||
self.swap_title_author_button = QToolButton(self)
|
||||
|
|
@ -117,7 +127,7 @@ def create_basic_metadata_widgets(self): # {{{
|
|||
self.cover = Cover(self)
|
||||
self.basic_metadata_widgets.append(self.cover)
|
||||
|
||||
self.comments = CommentsEdit(self)
|
||||
self.comments = CommentsEdit(self, self.one_line_comments_toolbar)
|
||||
self.basic_metadata_widgets.append(self.comments)
|
||||
|
||||
self.rating = RatingEdit(self)
|
||||
|
|
@ -156,129 +166,48 @@ def create_custom_metadata_widgets(self): # {{{
|
|||
w.setLayout(layout)
|
||||
self.custom_metadata_widgets, self.__cc_spacers = \
|
||||
populate_metadata_page(layout, self.db, None, parent=w, bulk=False,
|
||||
two_column=tweaks['metadata_single_use_2_cols_for_custom_fields'])
|
||||
two_column=self.cc_two_column)
|
||||
self.__custom_col_layouts = [layout]
|
||||
ans = self.custom_metadata_widgets
|
||||
for i in range(len(ans)-1):
|
||||
if len(ans[i+1].widgets) == 2:
|
||||
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
|
||||
else:
|
||||
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0])
|
||||
for c in range(2, len(ans[i].widgets), 2):
|
||||
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
||||
# }}}
|
||||
|
||||
def do_layout(self): # {{{
|
||||
self.central_widget.clear()
|
||||
self.tabs = []
|
||||
self.labels = []
|
||||
self.tabs.append(QWidget(self))
|
||||
self.central_widget.addTab(self.tabs[0], _("&Basic metadata"))
|
||||
self.tabs[0].l = l = QVBoxLayout()
|
||||
self.tabs[0].tl = tl = QGridLayout()
|
||||
self.tabs[0].setLayout(l)
|
||||
w = getattr(self, 'custom_metadata_widgets_parent', None)
|
||||
if w is not None:
|
||||
self.tabs.append(w)
|
||||
self.central_widget.addTab(w, _('&Custom metadata'))
|
||||
l.addLayout(tl)
|
||||
|
||||
def set_custom_metadata_tab_order(self, before=None, after=None): # {{{
|
||||
sto = QWidget.setTabOrder
|
||||
sto(self.fetch_metadata_button, self.title)
|
||||
|
||||
def create_row(row, one, two, three, col=1, icon='forward.png'):
|
||||
ql = BuddyLabel(one)
|
||||
tl.addWidget(ql, row, col+0, 1, 1)
|
||||
self.labels.append(ql)
|
||||
tl.addWidget(one, row, col+1, 1, 1)
|
||||
if two is not None:
|
||||
tl.addWidget(two, row, col+2, 1, 1)
|
||||
two.setIcon(QIcon(I(icon)))
|
||||
ql = BuddyLabel(three)
|
||||
tl.addWidget(ql, row, col+3, 1, 1)
|
||||
self.labels.append(ql)
|
||||
tl.addWidget(three, row, col+4, 1, 1)
|
||||
sto(one, two)
|
||||
sto(two, three)
|
||||
|
||||
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
|
||||
|
||||
create_row(0, self.title, self.deduce_title_sort_button, self.title_sort)
|
||||
sto(self.title_sort, self.authors)
|
||||
create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort)
|
||||
sto(self.author_sort, self.series)
|
||||
create_row(2, self.series, self.remove_unused_series_button,
|
||||
self.series_index, icon='trash.png')
|
||||
sto(self.series_index, self.swap_title_author_button)
|
||||
|
||||
tl.addWidget(self.formats_manager, 0, 6, 3, 1)
|
||||
|
||||
self.splitter = QSplitter(Qt.Horizontal, self)
|
||||
self.splitter.addWidget(self.cover)
|
||||
l.addWidget(self.splitter)
|
||||
self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self)
|
||||
gb.l = l = QGridLayout()
|
||||
gb.setLayout(l)
|
||||
for i, b in enumerate(self.cover.buttons[:3]):
|
||||
l.addWidget(b, 0, i, 1, 1)
|
||||
gb.hl = QHBoxLayout()
|
||||
for b in self.cover.buttons[3:]:
|
||||
gb.hl.addWidget(b)
|
||||
l.addLayout(gb.hl, 1, 0, 1, 3)
|
||||
self.tabs[0].middle = w = QWidget(self)
|
||||
w.l = l = QGridLayout()
|
||||
w.setLayout(w.l)
|
||||
l.setMargin(0)
|
||||
self.splitter.addWidget(w)
|
||||
def create_row2(row, widget, button=None):
|
||||
row += 1
|
||||
ql = BuddyLabel(widget)
|
||||
l.addWidget(ql, row, 0, 1, 1)
|
||||
l.addWidget(widget, row, 1, 1, 2 if button is None else 1)
|
||||
if button is not None:
|
||||
l.addWidget(button, row, 2, 1, 1)
|
||||
|
||||
l.addWidget(gb, 0, 0, 1, 3)
|
||||
self.tabs[0].spc_one = QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding)
|
||||
l.addItem(self.tabs[0].spc_one, 1, 0, 1, 3)
|
||||
create_row2(1, self.rating)
|
||||
create_row2(2, self.tags, self.tags_editor_button)
|
||||
create_row2(3, self.isbn)
|
||||
create_row2(4, self.timestamp, self.timestamp.clear_button)
|
||||
create_row2(5, self.pubdate, self.pubdate.clear_button)
|
||||
create_row2(6, self.publisher)
|
||||
self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding)
|
||||
l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3)
|
||||
l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3)
|
||||
|
||||
self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self)
|
||||
gb.l = l = QVBoxLayout()
|
||||
gb.setLayout(l)
|
||||
l.addWidget(self.comments)
|
||||
self.splitter.addWidget(gb)
|
||||
|
||||
if getattr(self, 'custom_metadata_widgets', []):
|
||||
ans = self.custom_metadata_widgets
|
||||
for i in range(len(ans)-1):
|
||||
if before is not None and i == 0:
|
||||
pass# Do something
|
||||
if len(ans[i+1].widgets) == 2:
|
||||
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
|
||||
else:
|
||||
sto(ans[i].widgets[-1], ans[i+1].widgets[0])
|
||||
for c in range(2, len(ans[i].widgets), 2):
|
||||
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
|
||||
if after is not None:
|
||||
pass # Do something
|
||||
# }}}
|
||||
|
||||
def __call__(self, id_, has_next=False, has_previous=False):
|
||||
# TODO: Next and previous buttons
|
||||
def do_layout(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __call__(self, id_):
|
||||
self.book_id = id_
|
||||
for widget in self.basic_metadata_widgets:
|
||||
widget.initialize(self.db, id_)
|
||||
for widget in self.custom_metadata_widgets:
|
||||
widget.initialize(id_)
|
||||
self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
|
||||
# Commented out as it doesn't play nice with Next, Prev buttons
|
||||
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
|
||||
# Miscellaneous interaction methods {{{
|
||||
def update_window_title(self, *args):
|
||||
title = self.title.current_val
|
||||
if len(title) > 50:
|
||||
title = title[:50] + u'\u2026'
|
||||
self.setWindowTitle(_('Edit Meta Information') + ' - ' +
|
||||
self.setWindowTitle(_('Edit Metadata') + ' - ' +
|
||||
title)
|
||||
|
||||
|
||||
def swap_title_author(self, *args):
|
||||
title = self.title.current_val
|
||||
self.title.current_val = authors_to_string(self.authors.current_val)
|
||||
|
|
@ -358,8 +287,10 @@ def update_from_mi(self, mi):
|
|||
|
||||
def fetch_metadata(self, *args):
|
||||
pass # TODO: fetch metadata
|
||||
# }}}
|
||||
|
||||
def apply_changes(self):
|
||||
self.changed.add(self.book_id)
|
||||
for widget in self.basic_metadata_widgets:
|
||||
try:
|
||||
if not widget.commit(self.db, self.book_id):
|
||||
|
|
@ -393,13 +324,307 @@ def reject(self):
|
|||
def save_state(self):
|
||||
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
|
||||
|
||||
# Dialog use methods {{{
|
||||
def start(self, row_list, current_row, view_slot=None):
|
||||
self.row_list = row_list
|
||||
self.current_row = current_row
|
||||
if view_slot is not None:
|
||||
self.view_format.connect(view_slot)
|
||||
self.do_one()
|
||||
ret = self.exec_()
|
||||
self.break_cycles()
|
||||
return ret
|
||||
|
||||
def do_one(self, delta=0):
|
||||
self.current_row += delta
|
||||
prev = next_ = None
|
||||
if self.current_row > 0:
|
||||
prev = self.db.title(self.row_list[self.current_row-1])
|
||||
if self.current_row < len(self.row_list) - 1:
|
||||
next_ = self.db.title(self.row_list[self.current_row+1])
|
||||
|
||||
if next_ is not None:
|
||||
tip = _('Save changes and edit the metadata of %s')%next_
|
||||
self.next_button.setToolTip(tip)
|
||||
self.next_button.setVisible(next_ is not None)
|
||||
if prev is not None:
|
||||
tip = _('Save changes and edit the metadata of %s')%prev
|
||||
self.prev_button.setToolTip(tip)
|
||||
self.prev_button.setVisible(prev is not None)
|
||||
self(self.db.id(self.row_list[self.current_row]))
|
||||
|
||||
def break_cycles(self):
|
||||
# Break any reference cycles that could prevent python
|
||||
# from garbage collecting this dialog
|
||||
def disconnect(signal):
|
||||
try:
|
||||
signal.disconnect()
|
||||
except:
|
||||
pass # Fails if view format was never connected
|
||||
disconnect(self.view_format)
|
||||
for b in ('next_button', 'prev_button'):
|
||||
x = getattr(self, b, None)
|
||||
if x is not None:
|
||||
disconnect(x.clicked)
|
||||
# }}}
|
||||
|
||||
class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
|
||||
|
||||
def do_layout(self):
|
||||
if len(self.db.custom_column_label_map) == 0:
|
||||
self.central_widget.tabBar().setVisible(False)
|
||||
self.central_widget.clear()
|
||||
self.tabs = []
|
||||
self.labels = []
|
||||
self.tabs.append(QWidget(self))
|
||||
self.central_widget.addTab(self.tabs[0], _("&Basic metadata"))
|
||||
self.tabs[0].l = l = QVBoxLayout()
|
||||
self.tabs[0].tl = tl = QGridLayout()
|
||||
self.tabs[0].setLayout(l)
|
||||
w = getattr(self, 'custom_metadata_widgets_parent', None)
|
||||
if w is not None:
|
||||
self.tabs.append(w)
|
||||
self.central_widget.addTab(w, _('&Custom metadata'))
|
||||
l.addLayout(tl)
|
||||
l.addItem(QSpacerItem(10, 15, QSizePolicy.Expanding,
|
||||
QSizePolicy.Fixed))
|
||||
|
||||
sto = QWidget.setTabOrder
|
||||
sto(self.button_box, self.fetch_metadata_button)
|
||||
sto(self.fetch_metadata_button, self.title)
|
||||
|
||||
def create_row(row, one, two, three, col=1, icon='forward.png'):
|
||||
ql = BuddyLabel(one)
|
||||
tl.addWidget(ql, row, col+0, 1, 1)
|
||||
self.labels.append(ql)
|
||||
tl.addWidget(one, row, col+1, 1, 1)
|
||||
if two is not None:
|
||||
tl.addWidget(two, row, col+2, 1, 1)
|
||||
two.setIcon(QIcon(I(icon)))
|
||||
ql = BuddyLabel(three)
|
||||
tl.addWidget(ql, row, col+3, 1, 1)
|
||||
self.labels.append(ql)
|
||||
tl.addWidget(three, row, col+4, 1, 1)
|
||||
sto(one, two)
|
||||
sto(two, three)
|
||||
|
||||
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
|
||||
|
||||
create_row(0, self.title, self.deduce_title_sort_button, self.title_sort)
|
||||
sto(self.title_sort, self.authors)
|
||||
create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort)
|
||||
sto(self.author_sort, self.series)
|
||||
create_row(2, self.series, self.remove_unused_series_button,
|
||||
self.series_index, icon='trash.png')
|
||||
sto(self.series_index, self.swap_title_author_button)
|
||||
|
||||
tl.addWidget(self.formats_manager, 0, 6, 3, 1)
|
||||
|
||||
self.splitter = QSplitter(Qt.Horizontal, self)
|
||||
self.splitter.addWidget(self.cover)
|
||||
l.addWidget(self.splitter)
|
||||
self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self)
|
||||
gb.l = l = QGridLayout()
|
||||
gb.setLayout(l)
|
||||
sto(self.swap_title_author_button, self.cover.buttons[0])
|
||||
for i, b in enumerate(self.cover.buttons[:3]):
|
||||
l.addWidget(b, 0, i, 1, 1)
|
||||
sto(b, self.cover.buttons[i+1])
|
||||
gb.hl = QHBoxLayout()
|
||||
for b in self.cover.buttons[3:]:
|
||||
gb.hl.addWidget(b)
|
||||
sto(self.cover.buttons[-2], self.cover.buttons[-1])
|
||||
l.addLayout(gb.hl, 1, 0, 1, 3)
|
||||
self.tabs[0].middle = w = QWidget(self)
|
||||
w.l = l = QGridLayout()
|
||||
w.setLayout(w.l)
|
||||
l.setMargin(0)
|
||||
self.splitter.addWidget(w)
|
||||
def create_row2(row, widget, button=None):
|
||||
row += 1
|
||||
ql = BuddyLabel(widget)
|
||||
l.addWidget(ql, row, 0, 1, 1)
|
||||
l.addWidget(widget, row, 1, 1, 2 if button is None else 1)
|
||||
if button is not None:
|
||||
l.addWidget(button, row, 2, 1, 1)
|
||||
if button is not None:
|
||||
sto(widget, button)
|
||||
|
||||
l.addWidget(gb, 0, 0, 1, 3)
|
||||
self.tabs[0].spc_one = QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding)
|
||||
l.addItem(self.tabs[0].spc_one, 1, 0, 1, 3)
|
||||
sto(self.cover.buttons[-1], self.rating)
|
||||
create_row2(1, self.rating)
|
||||
sto(self.rating, self.tags)
|
||||
create_row2(2, self.tags, self.tags_editor_button)
|
||||
sto(self.tags_editor_button, self.isbn)
|
||||
create_row2(3, self.isbn)
|
||||
sto(self.isbn, self.timestamp)
|
||||
create_row2(4, self.timestamp, self.timestamp.clear_button)
|
||||
sto(self.timestamp.clear_button, self.pubdate)
|
||||
create_row2(5, self.pubdate, self.pubdate.clear_button)
|
||||
sto(self.pubdate.clear_button, self.publisher)
|
||||
create_row2(6, self.publisher)
|
||||
self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding)
|
||||
l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3)
|
||||
l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3)
|
||||
|
||||
self.tabs[0].gb2 = gb = QGroupBox(_('Co&mments'), self)
|
||||
gb.l = l = QVBoxLayout()
|
||||
gb.setLayout(l)
|
||||
l.addWidget(self.comments)
|
||||
self.splitter.addWidget(gb)
|
||||
|
||||
self.set_custom_metadata_tab_order()
|
||||
|
||||
# }}}
|
||||
|
||||
class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
|
||||
|
||||
cc_two_column = False
|
||||
one_line_comments_toolbar = True
|
||||
|
||||
def do_layout(self):
|
||||
self.central_widget.clear()
|
||||
self.tabs = []
|
||||
self.labels = []
|
||||
sto = QWidget.setTabOrder
|
||||
|
||||
self.tabs.append(QWidget(self))
|
||||
self.central_widget.addTab(self.tabs[0], _("&Metadata"))
|
||||
self.tabs[0].l = QGridLayout()
|
||||
self.tabs[0].setLayout(self.tabs[0].l)
|
||||
|
||||
self.tabs.append(QWidget(self))
|
||||
self.central_widget.addTab(self.tabs[1], _("&Cover and formats"))
|
||||
self.tabs[1].l = QGridLayout()
|
||||
self.tabs[1].setLayout(self.tabs[1].l)
|
||||
|
||||
# Tab 0
|
||||
tab0 = self.tabs[0]
|
||||
|
||||
tl = QGridLayout()
|
||||
gb = QGroupBox(_('&Basic metadata'), self.tabs[0])
|
||||
self.tabs[0].l.addWidget(gb, 0, 0, 1, 1)
|
||||
gb.setLayout(tl)
|
||||
|
||||
sto(self.button_box, self.title)
|
||||
|
||||
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
|
||||
ql = BuddyLabel(widget)
|
||||
tl.addWidget(ql, row, 1, 1, 1)
|
||||
tl.addWidget(widget, row, 2, 1, 1)
|
||||
if button is not None:
|
||||
tl.addWidget(button, row, 3, span, 1)
|
||||
if icon is not None:
|
||||
button.setIcon(QIcon(I(icon)))
|
||||
if tab_to is not None:
|
||||
if button is not None:
|
||||
sto(widget, button)
|
||||
sto(button, tab_to)
|
||||
else:
|
||||
sto(widget, tab_to)
|
||||
|
||||
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
|
||||
|
||||
create_row(0, self.title, self.title_sort,
|
||||
button=self.deduce_title_sort_button, span=2,
|
||||
icon='auto_author_sort.png')
|
||||
create_row(1, self.title_sort, self.authors)
|
||||
create_row(2, self.authors, self.author_sort,
|
||||
button=self.deduce_author_sort_button,
|
||||
span=2, icon='auto_author_sort.png')
|
||||
create_row(3, self.author_sort, self.series)
|
||||
create_row(4, self.series, self.series_index,
|
||||
button=self.remove_unused_series_button, icon='trash.png')
|
||||
create_row(5, self.series_index, self.tags)
|
||||
create_row(6, self.tags, self.rating, button=self.tags_editor_button)
|
||||
create_row(7, self.rating, self.pubdate)
|
||||
create_row(8, self.pubdate, self.publisher,
|
||||
button=self.pubdate.clear_button, icon='trash.png')
|
||||
create_row(9, self.publisher, self.timestamp)
|
||||
create_row(10, self.timestamp, self.isbn,
|
||||
button=self.timestamp.clear_button, icon='trash.png')
|
||||
create_row(11, self.isbn, self.comments)
|
||||
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
|
||||
12, 1, 1 ,1)
|
||||
|
||||
w = getattr(self, 'custom_metadata_widgets_parent', None)
|
||||
if w is not None:
|
||||
gb = QGroupBox(_('C&ustom metadata'), tab0)
|
||||
gbl = QVBoxLayout()
|
||||
gb.setLayout(gbl)
|
||||
sr = QScrollArea(tab0)
|
||||
sr.setWidgetResizable(True)
|
||||
sr.setBackgroundRole(QPalette.Base)
|
||||
sr.setFrameStyle(QFrame.NoFrame)
|
||||
sr.setWidget(w)
|
||||
gbl.addWidget(sr)
|
||||
self.tabs[0].l.addWidget(gb, 0, 1, 1, 1)
|
||||
sto(self.isbn, gb)
|
||||
|
||||
w = QGroupBox(_('&Comments'), tab0)
|
||||
sp = QSizePolicy()
|
||||
sp.setVerticalStretch(10)
|
||||
sp.setHorizontalPolicy(QSizePolicy.Expanding)
|
||||
sp.setVerticalPolicy(QSizePolicy.Expanding)
|
||||
w.setSizePolicy(sp)
|
||||
l = QHBoxLayout()
|
||||
w.setLayout(l)
|
||||
l.addWidget(self.comments)
|
||||
tab0.l.addWidget(w, 1, 0, 1, 2)
|
||||
|
||||
# Tab 1
|
||||
tab1 = self.tabs[1]
|
||||
|
||||
wsp = QWidget(tab1)
|
||||
wgl = QVBoxLayout()
|
||||
wsp.setLayout(wgl)
|
||||
|
||||
# right-hand side of splitter
|
||||
gb = QGroupBox(_('Change cover'), tab1)
|
||||
l = QGridLayout()
|
||||
gb.setLayout(l)
|
||||
sto(self.swap_title_author_button, self.cover.buttons[0])
|
||||
for i, b in enumerate(self.cover.buttons[:3]):
|
||||
l.addWidget(b, 0, i, 1, 1)
|
||||
sto(b, self.cover.buttons[i+1])
|
||||
hl = QHBoxLayout()
|
||||
for b in self.cover.buttons[3:]:
|
||||
hl.addWidget(b)
|
||||
sto(self.cover.buttons[-2], self.cover.buttons[-1])
|
||||
l.addLayout(hl, 1, 0, 1, 3)
|
||||
wgl.addWidget(gb)
|
||||
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding))
|
||||
wgl.addWidget(self.fetch_metadata_button)
|
||||
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding))
|
||||
wgl.addWidget(self.formats_manager)
|
||||
|
||||
self.splitter = QSplitter(Qt.Horizontal, tab1)
|
||||
tab1.l.addWidget(self.splitter)
|
||||
self.splitter.addWidget(self.cover)
|
||||
self.splitter.addWidget(wsp)
|
||||
|
||||
self.formats_manager.formats.setMaximumWidth(10000)
|
||||
self.formats_manager.formats.setIconSize(QSize(64, 64))
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None):
|
||||
d = MetadataSingleDialog(db, parent)
|
||||
d.start(row_list, current_row, view_slot=view_slot)
|
||||
return d.changed
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
from calibre.library import db
|
||||
db = db()
|
||||
d = MetadataSingleDialog(db)
|
||||
d(db.data[0][0])
|
||||
d.exec_()
|
||||
from calibre.library import db as db_
|
||||
db = db_()
|
||||
row_list = list(range(len(db.data)))
|
||||
edit_metadata(db, row_list, 0)
|
||||
|
||||
|
|
|
|||
|
|
@ -114,6 +114,9 @@ def reread_collapse_parameters(self):
|
|||
|
||||
def set_database(self, db, tag_match, sort_by):
|
||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||
old = getattr(self, '_model', None)
|
||||
if old is not None:
|
||||
old.break_cycles()
|
||||
self._model = TagsModel(db, parent=self,
|
||||
hidden_categories=self.hidden_categories,
|
||||
search_restriction=None,
|
||||
|
|
@ -371,6 +374,9 @@ def recount(self, *args):
|
|||
# model. Reason: it is much easier than reconstructing the browser tree.
|
||||
def set_new_model(self, filter_categories_by=None):
|
||||
try:
|
||||
old = getattr(self, '_model', None)
|
||||
if old is not None:
|
||||
old.break_cycles()
|
||||
self._model = TagsModel(self.db, parent=self,
|
||||
hidden_categories=self.hidden_categories,
|
||||
search_restriction=self.search_restriction,
|
||||
|
|
@ -509,8 +515,8 @@ def __init__(self, db, parent, hidden_categories=None,
|
|||
QAbstractItemModel.__init__(self, parent)
|
||||
|
||||
# must do this here because 'QPixmap: Must construct a QApplication
|
||||
# before a QPaintDevice'. The ':' in front avoids polluting either the
|
||||
# user-defined categories (':' at end) or columns namespaces (no ':').
|
||||
# before a QPaintDevice'. The ':' at the end avoids polluting either of
|
||||
# the other namespaces (alpha, '#', or '@')
|
||||
iconmap = {}
|
||||
for key in category_icon_map:
|
||||
iconmap[key] = QIcon(I(category_icon_map[key]))
|
||||
|
|
@ -544,6 +550,9 @@ def __init__(self, db, parent, hidden_categories=None,
|
|||
tooltip=tt, category_key=r)
|
||||
self.refresh(data=data)
|
||||
|
||||
def break_cycles(self):
|
||||
self.db = self.root_item = None
|
||||
|
||||
def mimeTypes(self):
|
||||
return ["application/calibre+from_library"]
|
||||
|
||||
|
|
@ -681,7 +690,7 @@ def get_node_tree(self, sort):
|
|||
tb_cats = self.db.field_metadata
|
||||
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys(),
|
||||
key=sort_key):
|
||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
||||
cat_name = '@' + user_cat # add the '@' to avoid name collision
|
||||
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
||||
if len(saved_searches().names()):
|
||||
tb_cats.add_search_category(label='search', name=_('Searches'))
|
||||
|
|
@ -988,7 +997,7 @@ def tokens(self):
|
|||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||
continue
|
||||
row_index += 1
|
||||
if key.endswith(':'):
|
||||
if key.startswith('@'):
|
||||
# User category, so skip it. The tag will be marked in its real category
|
||||
continue
|
||||
category_item = self.root_item.children[row_index]
|
||||
|
|
@ -1007,7 +1016,7 @@ def tokens(self):
|
|||
ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
|
||||
return ans
|
||||
|
||||
def find_node(self, key, txt, start_path):
|
||||
def find_item_node(self, key, txt, start_path):
|
||||
'''
|
||||
Search for an item (a node) in the tags browser list that matches both
|
||||
the key (exact case-insensitive match) and txt (contains case-
|
||||
|
|
@ -1061,6 +1070,22 @@ def process_level(depth, category_index, start_path):
|
|||
break
|
||||
return self.path_found
|
||||
|
||||
def find_category_node(self, key):
|
||||
'''
|
||||
Search for an category node (a top-level node) in the tags browser list
|
||||
that matches the key (exact case-insensitive match). Returns the path to
|
||||
the node. Paths are as in find_item_node.
|
||||
'''
|
||||
if not key:
|
||||
return None
|
||||
|
||||
for i in xrange(self.rowCount(QModelIndex())):
|
||||
idx = self.index(i, 0, QModelIndex())
|
||||
ckey = idx.internalPointer().category_key
|
||||
if strcmp(ckey, key) == 0:
|
||||
return self.path_for_index(idx)
|
||||
return None
|
||||
|
||||
def show_item_at_path(self, path, box=False):
|
||||
'''
|
||||
Scroll the browser and open categories to show the item referenced by
|
||||
|
|
@ -1109,8 +1134,7 @@ class TagBrowserMixin(object): # {{{
|
|||
|
||||
def __init__(self, db):
|
||||
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
|
||||
self.tags_view.set_database(self.library_view.model().db,
|
||||
self.tag_match, self.sort_by)
|
||||
self.tags_view.set_database(db, self.tag_match, self.sort_by)
|
||||
self.tags_view.tags_marked.connect(self.search.set_search_string)
|
||||
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
||||
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
|
||||
|
|
@ -1347,15 +1371,15 @@ def find(self):
|
|||
self.search_button.setFocus(True)
|
||||
self.item_search.lineEdit().blockSignals(False)
|
||||
|
||||
colon = txt.find(':')
|
||||
key = None
|
||||
colon = txt.rfind(':') if len(txt) > 2 else 0
|
||||
if colon > 0:
|
||||
key = self.parent.library_view.model().db.\
|
||||
field_metadata.search_term_to_field_key(txt[:colon])
|
||||
txt = txt[colon+1:]
|
||||
|
||||
self.current_find_position = model.find_node(key, txt,
|
||||
self.current_find_position)
|
||||
self.current_find_position = \
|
||||
model.find_item_node(key, txt, self.current_find_position)
|
||||
if self.current_find_position:
|
||||
model.show_item_at_path(self.current_find_position, box=True)
|
||||
elif self.item_search.text():
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, out_format
|
|||
temp_files.append(d.cover_file)
|
||||
args = [in_file, out_file.name, recs]
|
||||
temp_files.append(out_file)
|
||||
jobs.append(('gui_convert', args, desc, d.output_format.upper(), book_id, temp_files))
|
||||
jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files))
|
||||
|
||||
changed = True
|
||||
d.break_cycles()
|
||||
|
|
@ -185,7 +185,7 @@ def do_book(self):
|
|||
|
||||
args = [in_file, out_file.name, lrecs]
|
||||
temp_files.append(out_file)
|
||||
self.jobs.append(('gui_convert', args, desc, self.output_format.upper(), book_id, temp_files))
|
||||
self.jobs.append(('gui_convert_override', args, desc, self.output_format.upper(), book_id, temp_files))
|
||||
|
||||
self.changed = True
|
||||
self.setValue(self.i)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
QPixmap, QMenu, QIcon, pyqtSignal, \
|
||||
QDialog, \
|
||||
QSystemTrayIcon, QApplication, QKeySequence, \
|
||||
QMessageBox, QHelpEvent
|
||||
QMessageBox, QHelpEvent, QAction
|
||||
|
||||
from calibre import prints
|
||||
from calibre.constants import __appname__, isosx
|
||||
|
|
@ -198,6 +198,10 @@ def initialize(self, library_path, db, listener, actions, show_gui=True):
|
|||
self.system_tray_icon.activated.connect(
|
||||
self.system_tray_icon_activated)
|
||||
|
||||
self.esc_action = QAction(self)
|
||||
self.addAction(self.esc_action)
|
||||
self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape))
|
||||
self.esc_action.triggered.connect(self.esc)
|
||||
|
||||
####################### Start spare job server ########################
|
||||
QTimer.singleShot(1000, self.add_spare_server)
|
||||
|
|
@ -294,6 +298,8 @@ def initialize(self, library_path, db, listener, actions, show_gui=True):
|
|||
'the file: %s<p>The '
|
||||
'log will be displayed automatically.')%self.gui_debug, show=True)
|
||||
|
||||
def esc(self, *args):
|
||||
self.search.clear()
|
||||
|
||||
def start_content_server(self):
|
||||
from calibre.library.server.main import start_threaded_server
|
||||
|
|
@ -305,7 +311,6 @@ def start_content_server(self):
|
|||
self.content_server.state_callback(True)
|
||||
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
|
||||
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
MainWindow.resizeEvent(self, ev)
|
||||
self.search.setMaximumWidth(self.width()-150)
|
||||
|
|
@ -440,6 +445,7 @@ def library_moved(self, newloc, copy_structure=False):
|
|||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
olddb.break_cycles()
|
||||
if self.device_connected:
|
||||
self.set_books_in_library(self.booklists(), reset=True)
|
||||
self.refresh_ondevice()
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ def __init__(self, db):
|
|||
|
||||
def stop(self):
|
||||
self.keep_running = False
|
||||
# Break cycles so that this object doesn't hold references to db
|
||||
self.do_write = self.get_metadata_for_dump = self.clear_dirtied = \
|
||||
self.set_dirtied = self.db = None
|
||||
|
||||
def run(self):
|
||||
while self.keep_running:
|
||||
|
|
@ -185,6 +188,11 @@ def __init__(self, FIELD_MAP, field_metadata):
|
|||
self.build_date_relop_dict()
|
||||
self.build_numeric_relop_dict()
|
||||
|
||||
def break_cycles(self):
|
||||
self._data = self.field_metadata = self.FIELD_MAP = \
|
||||
self.numeric_search_relops = self.date_search_relops = \
|
||||
self.all_search_locations = None
|
||||
|
||||
|
||||
def __getitem__(self, row):
|
||||
return self._data[self._map_filtered[row]]
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ def migrate_preference(key, default):
|
|||
self.field_metadata.remove_dynamic_categories()
|
||||
tb_cats = self.field_metadata
|
||||
for user_cat in sorted(self.prefs.get('user_categories', {}).keys(), key=sort_key):
|
||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
||||
cat_name = '@' + user_cat # add the '@' to avoid name collision
|
||||
tb_cats.add_user_category(label=cat_name, name=user_cat)
|
||||
if len(saved_searches().names()):
|
||||
tb_cats.add_search_category(label='search', name=_('Searches'))
|
||||
|
|
@ -361,6 +361,10 @@ def migrate_preference(key, default):
|
|||
self.refresh()
|
||||
self.last_update_check = self.last_modified()
|
||||
|
||||
def break_cycles(self):
|
||||
self.data.break_cycles()
|
||||
self.data = self.field_metadata = self.prefs = self.listeners = \
|
||||
self.refresh_ondevice = None
|
||||
|
||||
def initialize_database(self):
|
||||
metadata_sqlite = open(P('metadata_sqlite.sql'), 'rb').read()
|
||||
|
|
@ -1239,7 +1243,7 @@ def get_categories(self, sort='name', ids=None, icon_map=None):
|
|||
if category in icon_map:
|
||||
icon = icon_map[label]
|
||||
else:
|
||||
icon = icon_map[':custom']
|
||||
icon = icon_map['custom:']
|
||||
icon_map[category] = icon
|
||||
|
||||
datatype = cat['datatype']
|
||||
|
|
@ -1335,20 +1339,19 @@ def get_categories(self, sort='name', ids=None, icon_map=None):
|
|||
if label in taglist and name in taglist[label]:
|
||||
items.append(taglist[label][name])
|
||||
# else: do nothing, to not include nodes w zero counts
|
||||
if len(items):
|
||||
cat_name = user_cat+':' # add the ':' to avoid name collision
|
||||
# Not a problem if we accumulate entries in the icon map
|
||||
if icon_map is not None:
|
||||
icon_map[cat_name] = icon_map[':user']
|
||||
if sort == 'popularity':
|
||||
categories[cat_name] = \
|
||||
sorted(items, key=lambda x: x.count, reverse=True)
|
||||
elif sort == 'name':
|
||||
categories[cat_name] = \
|
||||
sorted(items, key=lambda x: sort_key(x.sort))
|
||||
else:
|
||||
categories[cat_name] = \
|
||||
sorted(items, key=lambda x:x.avg_rating, reverse=True)
|
||||
cat_name = '@' + user_cat # add the '@' to avoid name collision
|
||||
# Not a problem if we accumulate entries in the icon map
|
||||
if icon_map is not None:
|
||||
icon_map[cat_name] = icon_map['user:']
|
||||
if sort == 'popularity':
|
||||
categories[cat_name] = \
|
||||
sorted(items, key=lambda x: x.count, reverse=True)
|
||||
elif sort == 'name':
|
||||
categories[cat_name] = \
|
||||
sorted(items, key=lambda x: sort_key(x.sort))
|
||||
else:
|
||||
categories[cat_name] = \
|
||||
sorted(items, key=lambda x:x.avg_rating, reverse=True)
|
||||
|
||||
#### Finally, the saved searches category ####
|
||||
items = []
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class TagsIcons(dict):
|
|||
'''
|
||||
|
||||
category_icons = ['authors', 'series', 'formats', 'publisher', 'rating',
|
||||
'news', 'tags', ':custom', ':user', 'search',]
|
||||
'news', 'tags', 'custom:', 'user:', 'search',]
|
||||
def __init__(self, icon_dict):
|
||||
for a in self.category_icons:
|
||||
if a not in icon_dict:
|
||||
|
|
@ -31,8 +31,8 @@ def __init__(self, icon_dict):
|
|||
'rating' : 'rating.png',
|
||||
'news' : 'news.png',
|
||||
'tags' : 'tags.png',
|
||||
':custom' : 'column.png',
|
||||
':user' : 'drawer.png',
|
||||
'custom:' : 'column.png',
|
||||
'user:' : 'drawer.png',
|
||||
'search' : 'search.png'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -478,6 +478,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
|
|||
- Focus the search bar
|
||||
* - :kbd:`Shift+Ctrl+F`
|
||||
- Open the advanced search dialog
|
||||
* - :kbd:`Esc`
|
||||
- Clear the current search
|
||||
* - :kbd:`N or F3`
|
||||
- Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked)
|
||||
* - :kbd:`Shift+N or Shift+F3`
|
||||
|
|
@ -486,6 +488,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
|
|||
- Download metadata and shortcuts
|
||||
* - :kbd:`Ctrl+R`
|
||||
- Restart calibre
|
||||
* - :kbd:`Shift+Ctrl+E`
|
||||
- Add empty books to calibre
|
||||
* - :kbd:`Ctrl+Q`
|
||||
- Quit calibre
|
||||
|
||||
|
|
|
|||
|
|
@ -104,12 +104,12 @@ def run(self, message=None, file=None, verbose=False, selected_list=None,
|
|||
|
||||
def close_bug(self, bug, action, url, config):
|
||||
print 'Closing bug #%s'% bug
|
||||
nick = config.get_nickname()
|
||||
#nick = config.get_nickname()
|
||||
suffix = config.get_user_option('bug_close_comment')
|
||||
if suffix is None:
|
||||
suffix = 'The fix will be in the next release.'
|
||||
action = action+'ed'
|
||||
msg = '%s in branch %s. %s'%(action, nick, suffix)
|
||||
msg = '%s in branch %s. %s'%(action, 'lp:calibre', suffix)
|
||||
msg = msg.replace('Fixesed', 'Fixed')
|
||||
server = xmlrpclib.ServerProxy(url)
|
||||
server.ticket.update(int(bug), msg,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue