Merge from trunk

This commit is contained in:
Sengian 2010-07-24 09:33:45 +02:00
commit c1776406e5
77 changed files with 37880 additions and 33054 deletions

View File

@ -4,6 +4,64 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.10
date: 2010-07-23
new features:
- title: "Allow user customization of static resources such as icons and templates"
type: major
description: >
"You can now change the icons used in the User Interface and other static resources. Details on how to
do this are at: http://calibre-ebook.com/user_manual/customize.html#overriding-icons-templates-etcetera"
- title: "Split the 'Send to device' button into two buttons, 'Connect/share' and 'Send to device'. The new 'Send to device' button will now only be available when a device is connected."
- title: "Store column layout, saved searches and user categories seprately per calibre library. This makes it possible to easily switch between libraries with different custom column setups"
- title: "See the last modofied date for each format in the edit metadata dialog via a tooltip"
tickets: [6252]
- title: "PD Novel driver: Add support for uploading cover thumbnails to device"
- title: "More sophisticated metadata extraction from HTML files"
tickets: [6223]
bug fixes:
- title: "Fix problems with a few windows installs caused by the upgrade to Qt 4.6.3 in the previous release. These would manifest as a not working Add Books button, or deletes not actually deleting files, etc."
- title: "Restore configurability of toolbar, which was temporarily removed in 0.7.9. You can once again set icon size via Preferences->Interface"
- title: "Fix regression in iTunes driver in 0.7.9 when sending series info"
- title: "Search: Fix parsing of search terms that contain a word that starts with 'and' or 'or' and is not the first word"
- title: "When merging records also merge metadata in custom columns"
tickets: [6120]
- title: "When scrolling to show a particular row, handle the case when the first column is a custom column"
tickets: [6176]
- title: "Fix SD card detection for The Augen Book"
tickets: [6224]
- title: "CHM Input: Fix a couple of bugs that could cause crashes"
tickets: [6240]
- title: "Conversion pipeline: Handle zero width elements with non zero indents gracefully"
tickets: [6230]
new recipes:
- title: "daum.net"
author: trustin
- title: "MIT Technology Review, Alternet, Waco Tribune Herald and Orlando Sentinel"
author: rty
improved recipes:
- The BBC
- heise
- version: 0.7.9
date: 2010-07-17

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 48 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -3,14 +3,13 @@ __copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
news.bbc.co.uk
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class BBC(BasicNewsRecipe):
title = 'The BBC'
__author__ = 'Darko Miletic'
description = 'Global news and current affairs from the British Broadcasting Corporation'
title = 'BBC News'
__author__ = 'Darko Miletic, Starson17'
description = 'News from UK. '
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
@ -23,7 +22,6 @@ class BBC(BasicNewsRecipe):
publication_type = 'newsportal'
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} '
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
conversion_options = {
'comments' : description
,'tags' : category
@ -33,14 +31,15 @@ class BBC(BasicNewsRecipe):
}
keep_only_tags = [
dict(attrs={'id' :['meta-information','story-body']})
,dict(attrs={'class':['mxb' ,'storybody' ]})
dict(name='div', attrs={'class':['layout-block-a layout-block']})
,dict(attrs={'class':['story-body','storybody']})
]
remove_tags = [
dict(name=['object','link','table'])
,dict(attrs={'class':['caption','caption full-width','story-actions','hidden','sharesb','audioInStoryC']})
remove_tags = [
dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \
'story-feature wide ', 'story-feature narrow']})
]
remove_tags_after = dict(attrs={'class':'sharesb'})
remove_attributes = ['width','height']
feeds = [

View File

@ -8,7 +8,7 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class BBC(BasicNewsRecipe):
title = 'BBC News (fast)'
__author__ = 'Darko Miletic'
__author__ = 'Darko Miletic, Starson17'
description = 'News from UK. A much faster version that does not download pictures'
oldest_article = 2
max_articles_per_feed = 100
@ -31,14 +31,16 @@ class BBC(BasicNewsRecipe):
}
keep_only_tags = [
dict(attrs={'id' :['meta-information','story-body']})
,dict(attrs={'class':['mxb' ,'storybody' ]})
dict(name='div', attrs={'class':['layout-block-a layout-block']})
,dict(attrs={'class':['story-body','storybody']})
]
remove_tags = [
dict(name=['object','link','table','img'])
,dict(attrs={'class':['caption','caption full-width','story-actions','hidden','sharesb','audioInStoryC']})
remove_tags = [
dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \
'story-feature wide ', 'story-feature narrow']})
, dict(name=['img'])
]
remove_tags_after = dict(attrs={'class':'sharesb'})
remove_attributes = ['width','height']
feeds = [

View File

@ -0,0 +1,112 @@
import re
from datetime import date, timedelta
from calibre.web.feeds.recipes import BasicNewsRecipe
class MediaDaumRecipe(BasicNewsRecipe):
title = u'\uBBF8\uB514\uC5B4 \uB2E4\uC74C \uC624\uB298\uC758 \uC8FC\uC694 \uB274\uC2A4'
description = 'Articles from media.daum.net'
__author__ = 'trustin'
language = 'ko'
max_articles = 100
timefmt = ''
masthead_url = 'http://img-media.daum-img.net/2010ci/service_news.gif'
cover_margins = (18,18,'grey99')
no_stylesheets = True
remove_tags_before = dict(id='GS_con')
remove_tags_after = dict(id='GS_con')
remove_tags = [dict(attrs={'class':[
'bline',
'GS_vod',
]}),
dict(id=[
'GS_swf_poll',
'ad250',
]),
dict(name=['script', 'noscript', 'style', 'object'])]
preprocess_regexps = [
(re.compile(r'<\s+', re.DOTALL|re.IGNORECASE),
lambda match: '&lt; '),
(re.compile(r'(<br[^>]*>[ \t\r\n]*){3,}', re.DOTALL|re.IGNORECASE),
lambda match: ''),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</div>', re.DOTALL|re.IGNORECASE),
lambda match: '</div>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</p>', re.DOTALL|re.IGNORECASE),
lambda match: '</p>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</td>', re.DOTALL|re.IGNORECASE),
lambda match: '</td>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</strong>', re.DOTALL|re.IGNORECASE),
lambda match: '</strong>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</b>', re.DOTALL|re.IGNORECASE),
lambda match: '</b>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</em>', re.DOTALL|re.IGNORECASE),
lambda match: '</em>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</i>', re.DOTALL|re.IGNORECASE),
lambda match: '</i>'),
(re.compile(u'\(\uB05D\)[ \t\r\n]*<br[^>]*>.*</div>', re.DOTALL|re.IGNORECASE),
lambda match: '</div>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<div', re.DOTALL|re.IGNORECASE),
lambda match: '<div'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<p', re.DOTALL|re.IGNORECASE),
lambda match: '<p'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<table', re.DOTALL|re.IGNORECASE),
lambda match: '<table'),
(re.compile(r'<strong>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<strong>'),
(re.compile(r'<b>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<b>'),
(re.compile(r'<em>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<em>'),
(re.compile(r'<i>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<i>'),
(re.compile(u'(<br[^>]*>[ \t\r\n]*)*(\u25B6|\u25CF|\u261E|\u24D2|\(c\))*\[[^\]]*(\u24D2|\(c\)|\uAE30\uC0AC|\uC778\uAE30[^\]]*\uB274\uC2A4)[^\]]*\].*</div>', re.DOTALL|re.IGNORECASE),
lambda match: '</div>'),
]
def parse_index(self):
today = date.today();
articles = []
articles = self.parse_list_page(articles, today)
articles = self.parse_list_page(articles, today - timedelta(1))
return [('\uBBF8\uB514\uC5B4 \uB2E4\uC74C \uC624\uB298\uC758 \uC8FC\uC694 \uB274\uC2A4', articles)]
def parse_list_page(self, articles, date):
if len(articles) >= self.max_articles:
return articles
for page in range(1, 10):
soup = self.index_to_soup('http://media.daum.net/primary/total/list.html?cateid=100044&date=%(date)s&page=%(page)d' % {'date': date.strftime('%Y%m%d'), 'page': page})
done = True
for item in soup.findAll('dl'):
dt = item.find('dt', { 'class': 'tit' })
dd = item.find('dd', { 'class': 'txt' })
if dt is None:
break
a = dt.find('a', href=True)
url = 'http://media.daum.net/primary/total/' + a['href']
title = self.tag_to_string(dt)
if dd is None:
description = ''
else:
description = self.tag_to_string(dd)
articles.append(dict(title=title, description=description, url=url, content=''))
done = len(articles) >= self.max_articles
if done:
break
if done:
break
return articles
def preprocess_html(self, soup):
return self.strip_anchors(soup)
def strip_anchors(self, soup):
for para in soup.findAll(True):
aTags = para.findAll('a')
for a in aTags:
if a.img is None:
a.replaceWith(a.renderContents().decode('utf-8','replace'))
return soup

View File

@ -19,6 +19,32 @@ class heiseDe(BasicNewsRecipe):
max_articles_per_feed = 40
no_stylesheets = True
extra_css = '''
.bild_links, .bild_bu_links {
float:left;
line-height:105%;
margin:12px 1.4em 12px 0;
}
.bild_rechts, .bild_bu {
float:right;
line-height:105%;
margin:12px 0 12px 1em;
text-align:right;
}
.bild_zentriert {
clear:both;
line-height:105%;
margin:.2em auto;
text-align:center;
}
span.bild_links, span.bild_rechts, span.bild_zentriert {
display:block;
}
'''
remove_tags = [dict(id='navi_top'),
dict(id='navi_bottom'),
dict(id='logo'),

View File

@ -6,6 +6,7 @@ class AdvancedUserRecipe1257302745(BasicNewsRecipe):
language = 'en'
__author__ = 'onyxrev'
max_articles_per_feed = 100
no_stylesheets = True
remove_tags_before = {'class':'storytitle'}
remove_tags_after = dict(name='div', attrs={'id':'storytext' })

View File

@ -2,6 +2,7 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, os, re, logging, time, mimetypes, \
__builtin__, warnings, multiprocessing
from urllib import getproxies
@ -13,12 +14,13 @@ from functools import partial
warnings.simplefilter('ignore', DeprecationWarning)
from calibre.startup import plugins, winutil, winutilerror
from calibre.constants import iswindows, isosx, islinux, isfreebsd, isfrozen, \
terminal_controller, preferred_encoding, \
__appname__, __version__, __author__, \
win32event, win32api, winerror, fcntl, \
filesystem_encoding
filesystem_encoding, plugins, config_dir
from calibre.startup import winutil, winutilerror
import mechanize
if False:
@ -486,7 +488,6 @@ def ipython(user_ns=None):
sys.argv = ['ipython']
if user_ns is None:
user_ns = locals()
from calibre.utils.config import config_dir
ipydir = os.path.join(config_dir, ('_' if iswindows else '.')+'ipython')
os.environ['IPYTHONDIR'] = ipydir
if not os.path.exists(ipydir):

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.9'
__version__ = '0.7.10'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re
@ -14,7 +14,7 @@ numeric_version = tuple(_ver)
Various run time constants.
'''
import sys, locale, codecs
import sys, locale, codecs, os
from calibre.utils.terminfo import TerminalController
terminal_controller = TerminalController(sys.stdout)
@ -47,7 +47,7 @@ def debug():
global DEBUG
DEBUG = True
################################################################################
# plugins {{{
plugins = None
if plugins is None:
# Load plugins
@ -80,3 +80,22 @@ if plugins is None:
return plugins
plugins = load_plugins()
# }}}
# config_dir {{{
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
elif iswindows:
if plugins['winutil'][0] is None:
raise Exception(plugins['winutil'][1])
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
if not os.access(config_dir, os.W_OK|os.X_OK):
config_dir = os.path.expanduser('~')
config_dir = os.path.join(config_dir, 'calibre')
elif isosx:
config_dir = os.path.expanduser('~/Library/Preferences/calibre')
else:
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
config_dir = os.path.join(bdir, 'calibre')
# }}}

View File

@ -2668,7 +2668,7 @@ class ITUNES(DriverBase):
index = metadata.series_index
integer = int(index)
fraction = index-integer
series_index = '%04d%%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
if lb_added:
lb_added.SortName = "%s %s" % (metadata.series, series_index)
lb_added.Genre = metadata.series

View File

@ -43,6 +43,7 @@ class THEBOOK(N516):
BCD = [0x399]
MAIN_MEMORY_VOLUME_LABEL = 'The Book Main Memory'
EBOOK_DIR_MAIN = 'My books'
WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE'
class ALEX(N516):

View File

@ -6,6 +6,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from calibre.devices.usbms.driver import USBMS
class PALMPRE(USBMS):
@ -83,7 +85,14 @@ class PDNOVEL(USBMS):
VENDOR_NAME = 'ANDROID'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
THUMBNAIL_HEIGHT = 144
EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False
def upload_cover(self, path, filename, metadata):
coverdata = getattr(metadata, 'thumbnail', None)
if coverdata and coverdata[2]:
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
coverfile.write(coverdata[2])

View File

@ -177,6 +177,7 @@ class CHMInput(InputFormatPlugin):
chapter_path = None
if match_string(node.tag, 'object') and match_string(node.attrib['type'], 'text/sitemap'):
chapter_title = None
for child in node:
if match_string(child.tag,'param') and match_string(child.attrib['name'], 'name'):
chapter_title = child.attrib['value']

View File

@ -295,11 +295,10 @@ class MetaInformation(object):
if val is not None:
setattr(self, attr, val)
if mi.tags:
if replace_metadata:
self.tags = mi.tags
else:
self.tags += mi.tags
if replace_metadata:
self.tags = mi.tags
elif mi.tags:
self.tags += mi.tags
self.tags = list(set(self.tags))
if mi.author_sort_map:
@ -313,14 +312,17 @@ class MetaInformation(object):
if len(other_cover) > len(self_cover):
self.cover_data = mi.cover_data
my_comments = getattr(self, 'comments', '')
other_comments = getattr(mi, 'comments', '')
if not my_comments:
my_comments = ''
if not other_comments:
other_comments = ''
if len(other_comments.strip()) > len(my_comments.strip()):
self.comments = other_comments
if replace_metadata:
self.comments = getattr(mi, 'comments', '')
else:
my_comments = getattr(self, 'comments', '')
other_comments = getattr(mi, 'comments', '')
if not my_comments:
my_comments = ''
if not other_comments:
other_comments = ''
if len(other_comments.strip()) > len(my_comments.strip()):
self.comments = other_comments
other_lang = getattr(mi, 'language', None)
if other_lang and other_lang.lower() != 'und':

View File

@ -12,11 +12,15 @@ import re
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.chardet import xml_to_unicode
from calibre import entity_to_unicode
from calibre.utils.date import parse_date
def get_metadata(stream):
src = stream.read()
return get_metadata_(src)
def get_meta_regexp_(name):
return re.compile('<meta name=[\'"]' + name + '[\'"] content=[\'"](.+?)[\'"]\s*/?>', re.IGNORECASE)
def get_metadata_(src, encoding=None):
if not isinstance(src, unicode):
if not encoding:
@ -24,6 +28,9 @@ def get_metadata_(src, encoding=None):
else:
src = src.decode(encoding, 'replace')
# Meta data definitions as in
# http://www.mobileread.com/forums/showpost.php?p=712544&postcount=9
# Title
title = None
pat = re.compile(r'<!--.*?TITLE=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL)
@ -35,6 +42,13 @@ def get_metadata_(src, encoding=None):
match = pat.search(src)
if match:
title = match.group(1)
if not title:
for x in ('Title','DC.title','DCTERMS.title'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
title = match.group(1)
break
# Author
author = None
@ -42,7 +56,15 @@ def get_metadata_(src, encoding=None):
match = pat.search(src)
if match:
author = match.group(2).replace(',', ';')
else:
for x in ('Author','DC.creator.aut','DCTERMS.creator.aut'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
author = match.group(1)
break
# Create MetaInformation with Title and Author
ent_pat = re.compile(r'&(\S+)?;')
if title:
title = ent_pat.sub(entity_to_unicode, title)
@ -51,18 +73,158 @@ def get_metadata_(src, encoding=None):
mi = MetaInformation(title, [author] if author else None)
# Publisher
publisher = None
pat = re.compile(r'<!--.*?PUBLISHER=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL)
match = pat.search(src)
if match:
mi.publisher = match.group(2)
publisher = match.group(2)
else:
for x in ('Publisher','DC.publisher','DCTERMS.publisher'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
publisher = match.group(1)
break
if publisher:
mi.publisher = ent_pat.sub(entity_to_unicode, publisher)
# ISBN
isbn = None
pat = re.compile(r'<!--.*?ISBN=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
isbn = match.group(1)
else:
for x in ('ISBN','DC.identifier.ISBN','DCTERMS.identifier.ISBN'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
isbn = match.group(1)
break
if isbn:
mi.isbn = re.sub(r'[^0-9xX]', '', isbn)
# LANGUAGE
language = None
pat = re.compile(r'<!--.*?LANGUAGE=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
language = match.group(1)
else:
for x in ('DC.language','DCTERMS.language'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
language = match.group(1)
break
if language:
mi.language = language
# PUBDATE
pubdate = None
pat = re.compile(r'<!--.*?PUBDATE=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
pubdate = match.group(1)
else:
for x in ('Pubdate','Date of publication','DC.date.published','DC.date.publication','DC.date.issued','DCTERMS.issued'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
pubdate = match.group(1)
break
if pubdate:
try:
mi.pubdate = parse_date(pubdate)
except:
pass
# TIMESTAMP
timestamp = None
pat = re.compile(r'<!--.*?TIMESTAMP=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
timestamp = match.group(1)
else:
for x in ('Timestamp','Date of creation','DC.date.created','DC.date.creation','DCTERMS.created'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
timestamp = match.group(1)
break
if timestamp:
try:
mi.timestamp = parse_date(timestamp)
except:
pass
# SERIES
series = None
pat = re.compile(r'<!--.*?SERIES=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
series = match.group(1)
else:
pat = get_meta_regexp_("Series")
match = pat.search(src)
if match:
series = match.group(1)
if series:
mi.series = ent_pat.sub(entity_to_unicode, series)
# RATING
rating = None
pat = re.compile(r'<!--.*?RATING=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
rating = match.group(1)
else:
pat = get_meta_regexp_("Rating")
match = pat.search(src)
if match:
rating = match.group(1)
if rating:
try:
mi.rating = float(rating)
if mi.rating < 0:
mi.rating = 0
if mi.rating > 5:
mi.rating /= 2.
if mi.rating > 5:
mi.rating = 0
except:
pass
# COMMENTS
comments = None
pat = re.compile(r'<!--.*?COMMENTS=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
comments = match.group(1)
else:
pat = get_meta_regexp_("Comments")
match = pat.search(src)
if match:
comments = match.group(1)
if comments:
mi.comments = ent_pat.sub(entity_to_unicode, comments)
# TAGS
tags = None
pat = re.compile(r'<!--.*?TAGS=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
tags = match.group(1)
else:
pat = get_meta_regexp_("Tags")
match = pat.search(src)
if match:
tags = match.group(1)
if tags:
mi.tags = [x.strip() for x in ent_pat.sub(entity_to_unicode,
tags).split(",")]
# Ready to return MetaInformation
return mi

View File

@ -262,8 +262,11 @@ class CSSFlattener(object):
indent = asfloat(style['text-indent'], 0)
left += margin
if (left + indent) < 0:
percent = (margin - indent) / style['width']
cssdict['margin-left'] = "%d%%" % (percent * 100)
try:
percent = (margin - indent) / style['width']
cssdict['margin-left'] = "%d%%" % (percent * 100)
except ZeroDivisionError:
pass
left -= indent
if 'display' in cssdict and cssdict['display'] == 'in-line':
cssdict['display'] = 'inline'

View File

@ -578,9 +578,7 @@ class DeleteAction(object): # {{{
if row is not None:
ci = view.model().index(row, 0)
if ci.isValid():
view.setCurrentIndex(ci)
sm = view.selectionModel()
sm.select(ci, sm.Select)
view.set_current_row(row)
else:
if not confirm('<p>'+_('The selected books will be '
'<b>permanently deleted</b> '
@ -806,11 +804,11 @@ class EditMetadataAction(object): # {{{
for src_id in src_ids:
src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True)
if src_mi.comments and orig_dest_comments != src_mi.comments:
if not dest_mi.comments or len(dest_mi.comments) == 0:
if not dest_mi.comments:
dest_mi.comments = src_mi.comments
else:
dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments)
if src_mi.title and src_mi.title and (not dest_mi.title or
if src_mi.title and (not dest_mi.title or
dest_mi.title == _('Unknown')):
dest_mi.title = src_mi.title
if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] ==
@ -821,8 +819,7 @@ class EditMetadataAction(object): # {{{
if not dest_mi.tags:
dest_mi.tags = src_mi.tags
else:
for tag in src_mi.tags:
dest_mi.tags.append(tag)
dest_mi.tags.extend(src_mi.tags)
if src_mi.cover and not dest_mi.cover:
dest_mi.cover = src_mi.cover
if not dest_mi.publisher:
@ -833,6 +830,44 @@ class EditMetadataAction(object): # {{{
dest_mi.series = src_mi.series
dest_mi.series_index = src_mi.series_index
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
for key in db.field_metadata: #loop thru all defined fields
if db.field_metadata[key]['is_custom']:
colnum = db.field_metadata[key]['colnum']
# Get orig_dest_comments before it gets changed
if db.field_metadata[key]['datatype'] == 'comments':
orig_dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
for src_id in src_ids:
dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
src_value = db.get_custom(src_id, num=colnum, index_is_id=True)
if db.field_metadata[key]['datatype'] == 'comments':
if src_value and src_value != orig_dest_value:
if not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
else:
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
if db.field_metadata[key]['datatype'] in \
('bool', 'int', 'float', 'rating', 'datetime') \
and not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'series' \
and not dest_value:
if src_value:
src_index = db.get_custom_extra(src_id, num=colnum, index_is_id=True)
db.set_custom(dest_id, src_value, num=colnum, extra=src_index)
if db.field_metadata[key]['datatype'] == 'text' \
and not db.field_metadata[key]['is_multiple'] \
and not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'text' \
and db.field_metadata[key]['is_multiple']:
if src_value:
if not dest_value:
dest_value = src_value
else:
dest_value.extend(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
# }}}
def edit_device_collections(self, view, oncard=None):

View File

@ -72,7 +72,14 @@ class DeviceJob(BaseJob): # {{{
if self._aborted:
return
self.failed = True
self._details = unicode(err) + '\n\n' + \
try:
ex = unicode(err)
except:
try:
ex = str(err).decode(preferred_encoding, 'replace')
except:
ex = repr(err)
self._details = ex + '\n\n' + \
traceback.format_exc()
self.exception = err
finally:
@ -395,8 +402,6 @@ class DeviceAction(QAction): # {{{
class DeviceMenu(QMenu): # {{{
fetch_annotations = pyqtSignal()
connect_to_folder = pyqtSignal()
connect_to_itunes = pyqtSignal()
disconnect_mounted_device = pyqtSignal()
def __init__(self, parent=None):
@ -408,26 +413,6 @@ class DeviceMenu(QMenu): # {{{
self.set_default_menu = QMenu(_('Set default send to device action'))
self.set_default_menu.setIcon(QIcon(I('config.svg')))
opts = email_config().parse()
default_account = None
if opts.accounts:
self.email_to_menu = self.addMenu(_('Email to')+'...')
keys = sorted(opts.accounts.keys())
for account in keys:
formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats
if default:
default_account = (dest, False, False, I('mail.svg'),
_('Email to')+' '+account)
action1 = DeviceAction(dest, False, False, I('mail.svg'),
_('Email to')+' '+account)
action2 = DeviceAction(dest, True, False, I('mail.svg'),
_('Email to')+' '+account+ _(' and delete from library'))
map(self.email_to_menu.addAction, (action1, action2))
map(self._memory.append, (action1, action2))
self.email_to_menu.addSeparator()
action1.a_s.connect(self.action_triggered)
action2.a_s.connect(self.action_triggered)
basic_actions = [
('main:', False, False, I('reader.svg'),
@ -457,13 +442,6 @@ class DeviceMenu(QMenu): # {{{
]
if default_account is not None:
for x in (basic_actions, delete_actions):
ac = list(default_account)
if x is delete_actions:
ac[1] = True
x.insert(1, tuple(ac))
for menu in (self, self.set_default_menu):
for actions, desc in (
(basic_actions, ''),
@ -502,21 +480,7 @@ class DeviceMenu(QMenu): # {{{
config['default_send_to_device_action'] = repr(action)
self.group.triggered.connect(self.change_default_action)
if opts.accounts:
self.addSeparator()
self.addMenu(self.email_to_menu)
self.addSeparator()
mitem = self.addAction(QIcon(I('document_open.svg')), _('Connect to folder'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
self.connect_to_folder_action = mitem
mitem = self.addAction(QIcon(I('devices/itunes.png')),
_('Connect to iTunes'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
self.connect_to_itunes_action = mitem
mitem = self.addAction(QIcon(I('eject.svg')), _('Eject device'))
mitem.setEnabled(False)
@ -638,6 +602,8 @@ class DeviceMixin(object): # {{{
self.device_error_dialog = error_dialog(self, _('Error'),
_('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal)
self.share_conn_menu.connect_to_folder.connect(self.connect_to_folder)
self.share_conn_menu.connect_to_itunes.connect(self.connect_to_itunes)
self.emailer = Emailer()
self.emailer.start()
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
@ -675,21 +641,20 @@ class DeviceMixin(object): # {{{
def create_device_menu(self):
self._sync_menu = DeviceMenu(self)
self.share_conn_menu.build_email_entries(self._sync_menu)
self.action_sync.setMenu(self._sync_menu)
self.connect(self._sync_menu,
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.dispatch_sync_event)
self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
self._sync_menu.connect_to_folder.connect(self.connect_to_folder)
self._sync_menu.connect_to_itunes.connect(self.connect_to_itunes)
self._sync_menu.disconnect_mounted_device.connect(self.disconnect_mounted_device)
if self.device_connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
self._sync_menu.connect_to_itunes_action.setEnabled(False)
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.connect_to_itunes_action.setEnabled(True)
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
def device_job_exception(self, job):
@ -726,16 +691,16 @@ class DeviceMixin(object): # {{{
def set_device_menu_items_state(self, connected):
if connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
self._sync_menu.connect_to_itunes_action.setEnabled(False)
self.share_conn_menu.connect_to_folder_action.setEnabled(False)
self.share_conn_menu.connect_to_itunes_action.setEnabled(False)
self._sync_menu.disconnect_mounted_device_action.setEnabled(True)
self._sync_menu.enable_device_actions(True,
self.device_manager.device.card_prefix(),
self.device_manager.device)
self.eject_action.setEnabled(True)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.connect_to_itunes_action.setEnabled(True)
self.share_conn_menu.connect_to_folder_action.setEnabled(True)
self.share_conn_menu.connect_to_itunes_action.setEnabled(True)
self._sync_menu.disconnect_mounted_device_action.setEnabled(False)
self._sync_menu.enable_device_actions(False)
self.eject_action.setEnabled(False)
@ -983,6 +948,8 @@ class DeviceMixin(object): # {{{
else:
self.status_bar.show_message(_('Sent by email:') + ', '.join(good),
5000)
if remove:
self.library_view.model().delete_books_by_id(remove)
def cover_to_thumbnail(self, data):
p = QPixmap()

View File

@ -10,7 +10,7 @@ import os
from PyQt4.Qt import QDialog
from calibre.gui2.dialogs.choose_library_ui import Ui_Dialog
from calibre.gui2 import error_dialog, choose_dir, warning_dialog
from calibre.gui2 import error_dialog, choose_dir
from calibre.constants import filesystem_encoding
from calibre import isbytestring, patheq
from calibre.utils.config import prefs
@ -62,12 +62,6 @@ class ChooseLibrary(QDialog, Ui_Dialog):
return True
def perform_action(self, ac, loc):
if ac in ('new', 'existing'):
warning_dialog(self.parent(), _('Custom columns'),
_('If you use custom columns and they differ between '
'libraries, you will have various problems. Best '
'to ensure you have the same custom columns in each '
'library.'), show=True)
if ac in ('new', 'existing'):
prefs['library_path'] = loc
self.callback(loc)

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
from PyQt4.Qt import QDialog
from PyQt4.Qt import Qt, QDialog
from calibre.gui2.dialogs.comments_dialog_ui import Ui_CommentsDialog
class CommentsDialog(QDialog, Ui_CommentsDialog):
@ -12,6 +12,11 @@ class CommentsDialog(QDialog, Ui_CommentsDialog):
QDialog.__init__(self, parent)
Ui_CommentsDialog.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
if text is not None:
self.textbox.setPlainText(text)
self.textbox.setTabChangesFocus(True)

View File

@ -195,22 +195,32 @@ class PluginModel(QAbstractItemModel):
class CategoryModel(QStringListModel):
CATEGORIES = [
('general', _('General'), 'dialog_information.svg'),
('interface', _('Interface'), 'lookfeel.svg'),
('conversion', _('Conversion'), 'convert.svg'),
('email', _('Email\nDelivery'), 'mail.svg'),
('add/save', _('Add/Save'), 'save.svg'),
('advanced', _('Advanced'), 'view.svg'),
('server', _('Content\nServer'), 'network-server.svg'),
('plugins', _('Plugins'), 'plugins.svg'),
]
def __init__(self, *args):
QStringListModel.__init__(self, *args)
self.setStringList([_('General'), _('Interface'), _('Conversion'),
_('Email\nDelivery'), _('Add/Save'),
_('Advanced'), _('Content\nServer'), _('Plugins')])
self.icons = list(map(QVariant, map(QIcon,
[I('dialog_information.svg'), I('lookfeel.svg'),
I('convert.svg'),
I('mail.svg'), I('save.svg'), I('view.svg'),
I('network-server.svg'), I('plugins.svg')])))
self.setStringList([x[1] for x in self.CATEGORIES])
def data(self, index, role):
if role == Qt.DecorationRole:
return self.icons[index.row()]
return QVariant(QIcon(I(self.CATEGORIES[index.row()][2])))
return QStringListModel.data(self, index, role)
def index_for_name(self, name):
for i, x in enumerate(self.CATEGORIES):
if x[0] == name:
return self.index(i)
return self.index(0)
class EmailAccounts(QAbstractTableModel):
def __init__(self, accounts):
@ -332,7 +342,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
def category_current_changed(self, n, p):
self.stackedWidget.setCurrentIndex(n.row())
def __init__(self, parent, library_view, server=None):
def __init__(self, parent, library_view, server=None,
initial_category='general'):
ResizableDialog.__init__(self, parent)
self._category_model = CategoryModel()
@ -461,7 +472,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.button_osx_symlinks.setVisible(isosx)
self.separate_cover_flow.setChecked(config['separate_cover_flow'])
self.setup_email_page()
self.category_view.setCurrentIndex(self.category_view.model().index(0))
self.delete_news.setEnabled(bool(self.sync_news.isChecked()))
self.connect(self.sync_news, SIGNAL('toggled(bool)'),
self.delete_news.setEnabled)
@ -488,6 +498,22 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.opt_gui_layout.setCurrentIndex(li)
self.opt_disable_animations.setChecked(config['disable_animations'])
self.opt_show_donate_button.setChecked(config['show_donate_button'])
idx = 0
for i, x in enumerate([(_('Small'), 'small'), (_('Medium'), 'medium'),
(_('Large'), 'large')]):
if x[1] == gprefs.get('toolbar_icon_size', 'medium'):
idx = i
self.opt_toolbar_icon_size.addItem(x[0], x[1])
self.opt_toolbar_icon_size.setCurrentIndex(idx)
idx = 0
for i, x in enumerate([(_('Automatic'), 'auto'), (_('Always'), 'always'),
(_('Never'), 'never')]):
if x[1] == gprefs.get('toolbar_text', 'auto'):
idx = i
self.opt_toolbar_text.addItem(x[0], x[1])
self.opt_toolbar_text.setCurrentIndex(idx)
self.category_view.setCurrentIndex(self.category_view.model().index_for_name(initial_category))
def check_port_value(self, *args):
port = self.port.value()
@ -857,6 +883,10 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['disable_animations'] = bool(self.opt_disable_animations.isChecked())
config['show_donate_button'] = bool(self.opt_show_donate_button.isChecked())
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
for x in ('toolbar_icon_size', 'toolbar_text'):
w = getattr(self, 'opt_'+x)
data = w.itemData(w.currentIndex()).toString()
gprefs[x] = unicode(data)
fmts = []
for i in range(self.viewer.count()):
if self.viewer.item(i).checkState() == Qt.Checked:
@ -942,6 +972,5 @@ if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
d=ConfigDialog(None, LibraryDatabase2('/tmp'))
d.category_view.setCurrentIndex(d.category_view.model().index(0))
d.show()
app.exec_()

View File

@ -346,21 +346,21 @@
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news">
<property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<item row="9" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news">
<property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<item row="10" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_6">
@ -377,7 +377,7 @@
</item>
</layout>
</item>
<item row="10" column="0" colspan="2">
<item row="11" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QGroupBox" name="groupBox">
@ -580,6 +580,41 @@
</property>
</widget>
</item>
<item row="7" 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>
</layout>
</widget>
<widget class="QWidget" name="page_6">

View File

@ -44,6 +44,11 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
QDialog.__init__(self, parent)
Ui_QCreateCustomColumn.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.simple_error = partial(error_dialog, self, show=True,
show_copy_button=False)
self.connect(self.button_box, SIGNAL("accepted()"), self.accept)

View File

@ -21,6 +21,10 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
QDialog.__init__(self, parent)
Ui_EditAuthorsDialog.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.buttonBox.accepted.connect(self.accepted)

View File

@ -27,10 +27,11 @@ from calibre.ebooks.metadata import string_to_authors, \
from calibre.ebooks.metadata.library_thing import cover_from_isbn
from calibre.ebooks.metadata.meta import get_metadata
from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt
from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
from calibre.gui2.dialogs.config.social import SocialMetadata
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre import strftime
class CoverFetcher(QThread):
@ -75,13 +76,20 @@ class CoverFetcher(QThread):
class Format(QListWidgetItem):
def __init__(self, parent, ext, size, path=None):
def __init__(self, parent, ext, size, path=None, timestamp=None):
self.path = path
self.ext = ext
self.size = float(size)/(1024*1024)
text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
text, parent, QListWidgetItem.UserType)
if timestamp is not None:
ts = timestamp.astimezone(local_tz)
t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
text = _('Last modified: %s')%t
self.setToolTip(text)
self.setStatusTip(text)
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
@ -151,14 +159,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
nfile = run_plugins_on_import(_file)
if nfile is not None:
_file = nfile
size = os.stat(_file).st_size
stat = os.stat(_file)
size = stat.st_size
ext = os.path.splitext(_file)[1].lower().replace('.', '')
timestamp = utcfromtimestamp(stat.st_mtime)
for row in range(self.formats.count()):
fmt = self.formats.item(row)
if fmt.ext.lower() == ext:
self.formats.takeItem(row)
break
Format(self.formats, ext, size, path=_file)
Format(self.formats, ext, size, path=_file, timestamp=timestamp)
self.formats_changed = True
added = True
if bad_perms:
@ -379,9 +389,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if not ext:
ext = ''
size = self.db.sizeof_format(row, ext)
timestamp = self.db.format_last_modified(self.id, ext)
if size is None:
continue
Format(self.formats, ext, size)
Format(self.formats, ext, size, timestamp=timestamp)
self.initialize_combos()

View File

@ -25,8 +25,8 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
self.current_search_name = None
self.searches = {}
self.searches_to_delete = []
for name in saved_searches.names():
self.searches[name] = saved_searches.lookup(name)
for name in saved_searches().names():
self.searches[name] = saved_searches().lookup(name)
self.populate_search_list()
if initial_search is not None and initial_search in self.searches:
@ -78,7 +78,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
for name in self.searches_to_delete:
saved_searches.delete(name)
saved_searches().delete(name)
for name in self.searches:
saved_searches.add(name, self.searches[name])
saved_searches().add(name, self.searches[name])
QDialog.accept(self)

View File

@ -62,6 +62,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.search_done)
self.disconnect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
self.search.search_done)
self.search.search.disconnect()
self.recipe_model = None
def search_done(self, *args):

View File

@ -7,7 +7,6 @@ from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
from calibre.utils.config import prefs
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.constants import islinux
@ -29,6 +28,11 @@ class TagCategories(QDialog, Ui_TagCategories):
Ui_TagCategories.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.db = db
self.applied_items = []
@ -63,7 +67,7 @@ class TagCategories(QDialog, Ui_TagCategories):
self.all_items.append(t)
self.all_items_dict[label+':'+n] = t
self.categories = dict.copy(prefs['user_categories'])
self.categories = dict.copy(db.prefs.get('user_categories', {}))
if self.categories is None:
self.categories = {}
for cat in self.categories:
@ -182,7 +186,7 @@ class TagCategories(QDialog, Ui_TagCategories):
def accept(self):
self.save_category()
prefs['user_categories'] = self.categories
self.db.prefs['user_categories'] = self.categories
QDialog.accept(self)
def save_category(self):

View File

@ -43,6 +43,10 @@ class TagListEditor(QDialog, Ui_TagListEditor):
QDialog.__init__(self, window)
Ui_TagListEditor.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.to_rename = {}
self.to_delete = []

View File

@ -59,6 +59,7 @@ class LibraryViewMixin(object): # {{{
self.action_open_containing_folder,
self.action_show_book_details,
self.action_del,
self.action_conn_share,
add_to_library = None,
edit_device_collections=None,
similar_menu=similar_menu)
@ -67,21 +68,24 @@ class LibraryViewMixin(object): # {{{
edit_device_collections = (_('Manage collections'),
partial(self.edit_device_collections, oncard=None))
self.memory_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del,
self.action_view, self.action_save, None, None,
self.action_del, None,
add_to_library=add_to_library,
edit_device_collections=edit_device_collections)
edit_device_collections = (_('Manage collections'),
partial(self.edit_device_collections, oncard='carda'))
self.card_a_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del,
self.action_view, self.action_save, None, None,
self.action_del, None,
add_to_library=add_to_library,
edit_device_collections=edit_device_collections)
edit_device_collections = (_('Manage collections'),
partial(self.edit_device_collections, oncard='cardb'))
self.card_b_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del,
self.action_view, self.action_save, None, None,
self.action_del, None,
add_to_library=add_to_library,
edit_device_collections=edit_device_collections)

View File

@ -16,14 +16,14 @@ from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \
from calibre.constants import __appname__, isosx
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2 import config, open_url
from calibre.gui2 import config, open_url, gprefs
from calibre.gui2.widgets import ComboBoxWithHelp
from calibre import human_readable
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2.dialogs.scheduler import Scheduler
from calibre.utils.smtp import config as email_config
ICON_SIZE = 48
class SaveMenu(QMenu): # {{{
@ -228,12 +228,11 @@ class ToolBar(QToolBar): # {{{
self.setFloatable(False)
self.setOrientation(Qt.Horizontal)
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
self.donate = donate
self.apply_settings()
self.all_actions = actions
self.donate = donate
self.location_manager = location_manager
self.location_manager.locations_changed.connect(self.build_bar)
self.d_widget = QWidget()
@ -242,6 +241,17 @@ class ToolBar(QToolBar): # {{{
donate.setAutoRaise(True)
donate.setCursor(Qt.PointingHandCursor)
self.build_bar()
self.preferred_width = self.sizeHint().width()
def apply_settings(self):
sz = gprefs.get('toolbar_icon_size', 'medium')
sz = {'small':24, 'medium':48, 'large':64}[sz]
self.setIconSize(QSize(sz, sz))
style = Qt.ToolButtonTextUnderIcon
if gprefs.get('toolbar_text', 'auto') == 'never':
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
self.donate.set_normal_icon_size(sz, sz)
def contextMenuEvent(self, *args):
pass
@ -262,7 +272,9 @@ class ToolBar(QToolBar): # {{{
ch.setCursor(Qt.PointingHandCursor)
ch.setAutoRaise(True)
if ac.menu() is not None:
ch.setPopupMode(ch.MenuButtonPopup)
name = getattr(ac, 'action_name', None)
ch.setPopupMode(ch.InstantPopup if name == 'conn_share'
else ch.MenuButtonPopup)
for x in actions:
self.addAction(x)
@ -292,11 +304,16 @@ class ToolBar(QToolBar): # {{{
a.setText(text)
def resizeEvent(self, ev):
style = Qt.ToolButtonTextUnderIcon
if self.size().width() < 1260:
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
QToolBar.resizeEvent(self, ev)
style = Qt.ToolButtonTextUnderIcon
p = gprefs.get('toolbar_text', 'auto')
if p == 'never':
style = Qt.ToolButtonIconOnly
if p == 'auto' and self.preferred_width > self.width()+35:
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
def database_changed(self, db):
pass
@ -306,6 +323,62 @@ class ToolBar(QToolBar): # {{{
class Action(QAction):
pass
class ShareConnMenu(QMenu): # {{{
connect_to_folder = pyqtSignal()
connect_to_itunes = pyqtSignal()
config_email = pyqtSignal()
def __init__(self, parent=None):
QMenu.__init__(self, parent)
mitem = self.addAction(QIcon(I('devices/folder.svg')), _('Connect to folder'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
self.connect_to_folder_action = mitem
mitem = self.addAction(QIcon(I('devices/itunes.png')),
_('Connect to iTunes'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
self.connect_to_itunes_action = mitem
self.addSeparator()
self.email_actions = []
def build_email_entries(self, sync_menu):
from calibre.gui2.device import DeviceAction
for ac in self.email_actions:
self.removeAction(ac)
self.email_actions = []
opts = email_config().parse()
if opts.accounts:
self.email_to_menu = QMenu(_('Email to')+'...', self)
keys = sorted(opts.accounts.keys())
for account in keys:
formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats
action1 = DeviceAction(dest, False, False, I('mail.svg'),
_('Email to')+' '+account)
action2 = DeviceAction(dest, True, False, I('mail.svg'),
_('Email to')+' '+account+ _(' and delete from library'))
map(self.email_to_menu.addAction, (action1, action2))
if default:
map(self.addAction, (action1, action2))
map(self.email_actions.append, (action1, action2))
self.email_to_menu.addSeparator()
action1.a_s.connect(sync_menu.action_triggered)
action2.a_s.connect(sync_menu.action_triggered)
ac = self.addMenu(self.email_to_menu)
self.email_actions.append(ac)
else:
ac = self.addAction(_('Setup email based sharing of books'))
self.email_actions.append(ac)
ac.triggered.connect(self.setup_email)
def setup_email(self, *args):
self.config_email.emit()
# }}}
class MainWindowMixin(object):
def __init__(self, db):
@ -321,7 +394,6 @@ class MainWindowMixin(object):
self.centralwidget.setLayout(self._central_widget_layout)
self.resize(1012, 740)
self.donate_button = ThrobbingButton(self.centralwidget)
self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
self.location_manager = LocationManager(self)
self.init_scheduler(db)
@ -341,7 +413,6 @@ class MainWindowMixin(object):
self.scheduler.start_recipe_fetch.connect(
self.download_scheduled_recipe, type=Qt.QueuedConnection)
def read_toolbar_settings(self):
pass
@ -372,18 +443,19 @@ class MainWindowMixin(object):
setattr(self, 'action_'+name, action)
all_actions.append(action)
ac(0, 7, 0, 'add', _('Add books'), 'add_book.svg', _('A'))
ac(0, 0, 0, 'add', _('Add books'), 'add_book.svg', _('A'))
ac(1, 1, 0, 'edit', _('Edit metadata'), 'edit_input.svg', _('E'))
ac(2, 2, 3, 'convert', _('Convert books'), 'convert.svg', _('C'))
ac(3, 3, 0, 'view', _('View'), 'view.svg', _('V'))
ac(4, 4, 3, 'choose_library', _('%d books')%0, 'lt.png',
ac(-1, 4, 0, 'sync', _('Send to device'), 'sync.svg')
ac(5, 5, 3, 'choose_library', _('%d books')%0, 'lt.png',
tooltip=_('Choose calibre library to work with'))
ac(5, 5, 3, 'news', _('Fetch news'), 'news.svg', _('F'))
ac(6, 6, 0, 'save', _('Save to disk'), 'save.svg', _('S'))
ac(7, 0, 0, 'sync', _('Send to device'), 'sync.svg')
ac(8, 8, 3, 'del', _('Remove books'), 'trash.svg', _('Del'))
ac(9, 9, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
ac(10, 10, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
ac(6, 6, 3, 'news', _('Fetch news'), 'news.svg', _('F'))
ac(7, 7, 0, 'save', _('Save to disk'), 'save.svg', _('S'))
ac(8, 8, 0, 'conn_share', _('Connect/share'), 'connect_share.svg')
ac(9, 9, 3, 'del', _('Remove books'), 'trash.svg', _('Del'))
ac(10, 10, 3, 'help', _('Help'), 'help.svg', _('F1'), _("Browse the calibre User Manual"))
ac(11, 11, 0, 'preferences', _('Preferences'), 'config.svg', _('Ctrl+P'))
ac(-1, -1, 0, 'merge', _('Merge book records'), 'merge_books.svg', _('M'))
ac(-1, -1, 0, 'open_containing_folder', _('Open containing folder'),
@ -402,6 +474,10 @@ class MainWindowMixin(object):
self.action_news.setMenu(self.scheduler.news_menu)
self.action_news.triggered.connect(
self.scheduler.show_dialog)
self.share_conn_menu = ShareConnMenu(self)
self.share_conn_menu.config_email.connect(partial(self.do_config,
initial_category='email'))
self.action_conn_share.setMenu(self.share_conn_menu)
self.action_help.triggered.connect(self.show_help)
md = QMenu()
@ -528,6 +604,7 @@ class MainWindowMixin(object):
for x in (self.preferences_action, self.action_preferences):
x.triggered.connect(self.do_config)
return all_actions
# }}}

View File

@ -214,13 +214,17 @@ class BooksView(QTableView): # {{{
state['column_sizes'][name] = h.sectionSize(i)
return state
def write_state(self, state):
db = getattr(self.model(), 'db', None)
name = unicode(self.objectName())
if name and db is not None:
db.prefs.set(name + ' books view state', state)
def save_state(self):
# Only save if we have been initialized (set_database called)
if len(self.column_map) > 0 and self.was_restored:
state = self.get_state()
name = unicode(self.objectName())
if name:
gprefs.set(name + ' books view state', state)
self.write_state(state)
def cleanup_sort_history(self, sort_history):
history = []
@ -298,11 +302,27 @@ class BooksView(QTableView): # {{{
old_state['column_sizes'][name] += 12
return old_state
def restore_state(self):
def get_old_state(self):
ans = None
name = unicode(self.objectName())
old_state = None
if name:
old_state = gprefs.get(name + ' books view state', None)
name += ' books view state'
db = getattr(self.model(), 'db', None)
if db is not None:
ans = db.prefs.get(name, None)
if ans is None:
ans = gprefs.get(name, None)
try:
del gprefs[name]
except:
pass
if ans is not None:
db.prefs[name] = ans
return ans
def restore_state(self):
old_state = self.get_old_state()
if old_state is None:
old_state = self.get_default_state()
@ -370,7 +390,7 @@ class BooksView(QTableView): # {{{
# Context Menu {{{
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
save, open_folder, book_details, delete,
save, open_folder, book_details, delete, conn_share,
similar_menu=None, add_to_library=None,
edit_device_collections=None):
self.setContextMenuPolicy(Qt.DefaultContextMenu)
@ -381,6 +401,8 @@ class BooksView(QTableView): # {{{
self.context_menu.addAction(send_to_device)
if convert is not None:
self.context_menu.addAction(convert)
if conn_share is not None:
self.context_menu.addAction(conn_share)
self.context_menu.addAction(view)
self.context_menu.addAction(save)
if open_folder is not None:
@ -456,14 +478,20 @@ class BooksView(QTableView): # {{{
def set_current_row(self, row, select=True):
if row > -1:
h = self.horizontalHeader()
for i in range(h.count()):
if not h.isSectionHidden(i):
index = self.model().index(row, i)
self.setCurrentIndex(index)
if select:
sm = self.selectionModel()
sm.select(index, sm.ClearAndSelect|sm.Rows)
break
logical_indices = list(range(h.count()))
logical_indices = [x for x in logical_indices if not
h.isSectionHidden(x)]
pairs = [(x, h.visualIndex(x)) for x in logical_indices if
h.visualIndex(x) > -1]
if not pairs:
pairs = [(0, 0)]
pairs.sort(cmp=lambda x,y:cmp(x[1], y[1]))
i = pairs[0][0]
index = self.model().index(row, i)
self.setCurrentIndex(index)
if select:
sm = self.selectionModel()
sm.select(index, sm.ClearAndSelect|sm.Rows)
def close(self):
self._model.close()
@ -507,6 +535,19 @@ class DeviceBooksView(BooksView): # {{{
self.context_menu.popup(event.globalPos())
event.accept()
def get_old_state(self):
ans = None
name = unicode(self.objectName())
if name:
name += ' books view state'
ans = gprefs.get(name, None)
return ans
def write_state(self, state):
name = unicode(self.objectName())
if name:
gprefs.set(name + ' books view state', state)
def set_database(self, db):
self._model.set_database(db)
self.restore_state()

View File

@ -10,13 +10,12 @@ import re
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
pyqtSignal, SIGNAL, QObject, QDialog, QCompleter, \
QAction, QKeySequence
QAction, QKeySequence, QTimer
from calibre.gui2 import config
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.dialogs.search import SearchDialog
from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches
class SearchLineEdit(QLineEdit):
@ -83,7 +82,9 @@ class SearchBox2(QComboBox):
self.help_state = False
self.as_you_type = True
self.prev_search = ''
self.timer = None
self.timer = QTimer()
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.timer_event, type=Qt.QueuedConnection)
self.setInsertPolicy(self.NoInsert)
self.setMaxCount(self.MAX_COUNT)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
@ -117,9 +118,6 @@ class SearchBox2(QComboBox):
self.search.emit('')
self._in_a_search = False
self.setEditText(self.help_text)
if self.timer is not None: # Turn off any timers that got started in setEditText
self.killTimer(self.timer)
self.timer = None
self.line_edit.home(False)
self.line_edit.setStyleSheet(
'QLineEdit { color: gray; background-color: %s; }' %
@ -148,18 +146,15 @@ class SearchBox2(QComboBox):
self._in_a_search = False
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self.do_search()
self.timer = self.startTimer(self.__class__.INTERVAL)
self.timer.start(1500)
def mouse_released(self, event):
self.normalize_state()
if self.as_you_type:
self.timer = self.startTimer(self.__class__.INTERVAL)
self.timer.start(1500)
def timerEvent(self, event):
self.killTimer(event.timerId())
if event.timerId() == self.timer:
self.timer = None
self.do_search()
def timer_event(self):
self.do_search()
def history_selected(self, text):
self.emit(SIGNAL('changed()'))
@ -213,9 +208,6 @@ class SearchBox2(QComboBox):
return
self.normalize_state()
self.setEditText(txt)
if self.timer is not None: # Turn off any timers that got started in setEditText
self.killTimer(self.timer)
self.timer = None
self.search.emit(txt)
self.line_edit.end(False)
self.initial_state = False
@ -259,8 +251,7 @@ class SavedSearchBox(QComboBox):
self.setMinimumContentsLength(10)
self.tool_tip_text = self.toolTip()
def initialize(self, _saved_searches, _search_box, colorize=False, help_text=_('Search')):
self.saved_searches = _saved_searches
def initialize(self, _search_box, colorize=False, help_text=_('Search')):
self.search_box = _search_box
self.help_text = help_text
self.colorize = colorize
@ -302,11 +293,11 @@ class SavedSearchBox(QComboBox):
self.normalize_state()
self.search_box.set_search_string(u'search:"%s"' % qname)
self.setEditText(qname)
self.setToolTip(self.saved_searches.lookup(qname))
self.setToolTip(saved_searches().lookup(qname))
def initialize_saved_search_names(self):
self.clear()
qnames = self.saved_searches.names()
qnames = saved_searches().names()
self.addItems(qnames)
self.setCurrentIndex(-1)
@ -319,10 +310,10 @@ class SavedSearchBox(QComboBox):
idx = self.currentIndex
if idx < 0:
return
ss = self.saved_searches.lookup(unicode(self.currentText()))
ss = saved_searches().lookup(unicode(self.currentText()))
if ss is None:
return
self.saved_searches.delete(unicode(self.currentText()))
saved_searches().delete(unicode(self.currentText()))
self.clear_to_help()
self.search_box.clear_to_help()
self.emit(SIGNAL('changed()'))
@ -332,8 +323,8 @@ class SavedSearchBox(QComboBox):
name = unicode(self.currentText())
if self.help_state or not name.strip():
name = unicode(self.search_box.text()).replace('"', '')
self.saved_searches.delete(name)
self.saved_searches.add(name, unicode(self.search_box.text()))
saved_searches().delete(name)
saved_searches().add(name, unicode(self.search_box.text()))
# now go through an initialization cycle to ensure that the combobox has
# the new search in it, that it is selected, and that the search box
# references the new search instead of the text in the search.
@ -348,7 +339,7 @@ class SavedSearchBox(QComboBox):
idx = self.currentIndex();
if idx < 0:
return
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
self.search_box.set_search_string(saved_searches().lookup(unicode(self.currentText())))
class SearchBoxMixin(object):
@ -390,11 +381,12 @@ class SearchBoxMixin(object):
class SavedSearchBoxMixin(object):
def __init__(self):
def __init__(self, db):
self.db = db
self.connect(self.saved_search, SIGNAL('changed()'), self.saved_searches_changed)
self.saved_searches_changed()
self.connect(self.clear_button, SIGNAL('clicked()'), self.saved_search.clear_to_help)
self.saved_search.initialize(saved_searches, self.search, colorize=True,
self.saved_search.initialize(self.search, colorize=True,
help_text=_('Saved Searches'))
self.connect(self.save_search_button, SIGNAL('clicked()'),
self.saved_search.save_search_button_clicked)
@ -409,9 +401,12 @@ class SavedSearchBoxMixin(object):
b = getattr(self, x+'_search_button')
b.setStatusTip(b.toolTip())
def set_database(self, db):
self.db = db
self.saved_searches_changed()
def saved_searches_changed(self):
p = prefs['saved_searches'].keys()
p = saved_searches().names()
p.sort()
t = unicode(self.search_restriction.currentText())
self.search_restriction.clear() # rebuild the restrictions combobox using current saved searches

View File

@ -17,7 +17,6 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog
@ -224,7 +223,7 @@ class TagsView(QTreeView): # {{{
# Always show the user categories editor
self.context_menu.addSeparator()
if category in prefs['user_categories'].keys():
if category in self.db.prefs.get('user_categories', {}).keys():
self.context_menu.addAction(_('Manage User Categories'),
partial(self.context_menu_handler, action='manage_categories',
category=category))
@ -426,10 +425,10 @@ class TagsModel(QAbstractItemModel): # {{{
for k in tb_cats.keys():
if tb_cats[k]['kind'] in ['user', 'search']:
del tb_cats[k]
for user_cat in sorted(prefs['user_categories'].keys()):
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys()):
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()):
if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
# Now get the categories
@ -507,11 +506,11 @@ class TagsModel(QAbstractItemModel): # {{{
if key not in self.db.field_metadata:
return
if key == 'search':
if val in saved_searches.names():
if val in saved_searches().names():
error_dialog(self.tags_view, _('Duplicate search name'),
_('The saved search name %s is already used.')%val).exec_()
return False
saved_searches.rename(unicode(item.data(role).toString()), val)
saved_searches().rename(unicode(item.data(role).toString()), val)
self.tags_view.search_item_renamed.emit()
else:
if key == 'series':

View File

@ -199,7 +199,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
UpdateMixin.__init__(self, opts)
####################### Search boxes ########################
SavedSearchBoxMixin.__init__(self)
SavedSearchBoxMixin.__init__(self, db)
SearchBoxMixin.__init__(self)
####################### Library view ########################
@ -351,7 +351,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
def do_config(self, *args):
def do_config(self, checked=False, initial_category='general'):
if self.job_manager.has_jobs():
d = error_dialog(self, _('Cannot configure'),
_('Cannot configure while there are running jobs.'))
@ -363,7 +363,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
d.exec_()
return
d = ConfigDialog(self, self.library_view,
server=self.content_server)
server=self.content_server, initial_category=initial_category)
d.exec_()
self.content_server = d.server
@ -380,6 +380,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.tags_view.recount()
self.create_device_menu()
self.set_device_menu_items_state(bool(self.device_connected))
self.tool_bar.apply_settings()
def library_moved(self, newloc):
if newloc is None: return
@ -392,6 +393,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.library_view.model().set_book_on_device_func(self.book_on_device)
self.status_bar.clear_message()
self.search.clear_to_help()
self.saved_search.clear_to_help()
self.book_details.reset_info()
self.library_view.model().count_changed()
self.scheduler.database_changed(db)

View File

@ -448,7 +448,7 @@ class DocumentView(QWebView):
self.unimplemented_actions = list(map(self.pageAction,
[d.DownloadImageToDisk, d.OpenLinkInNewWindow, d.DownloadLinkToDisk,
d.OpenImageInNewWindow, d.OpenLink]))
self.dictionary_action = QAction(QIcon(I('dictionary.png')),
self.dictionary_action = QAction(QIcon(I('dictionary.svg')),
_('&Lookup in dictionary'), self)
self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L)
self.dictionary_action.triggered.connect(self.lookup)

View File

@ -615,7 +615,7 @@ class ResultCache(SearchQueryParser):
q = self.search_restriction
else:
q = query
if not ignore_search_restriction:
if not ignore_search_restriction and self.search_restriction:
q = u'%s (%s)' % (self.search_restriction, query)
if not q:
if return_matches:

View File

@ -19,6 +19,7 @@ from calibre.library.schema_upgrades import SchemaUpgrade
from calibre.library.caches import ResultCache
from calibre.library.custom_columns import CustomColumns
from calibre.library.sqlite import connect, IntegrityError, DBThread
from calibre.library.prefs import DBPrefs
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
MetaInformation
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
@ -29,7 +30,7 @@ from calibre.customize.ui import run_plugins_on_import
from calibre.utils.filenames import ascii_filename
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
from calibre.utils.config import prefs, tweaks
from calibre.utils.search_query_parser import saved_searches
from calibre.utils.search_query_parser import saved_searches, set_saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.utils.magick_draw import save_cover_data_to
@ -140,6 +141,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.initialize_dynamic()
def initialize_dynamic(self):
self.prefs = DBPrefs(self)
# Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default):
oldval = prefs[key]
if oldval != default:
self.prefs[key] = oldval
prefs[key] = default
if key not in self.prefs:
self.prefs[key] = default
migrate_preference('user_categories', {})
migrate_preference('saved_searches', {})
set_saved_searches(self, 'saved_searches')
self.conn.executescript('''
DROP TRIGGER IF EXISTS author_insert_trg;
CREATE TEMP TRIGGER author_insert_trg
@ -268,10 +284,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for k in tb_cats.keys():
if tb_cats[k]['kind'] in ['user', 'search']:
del tb_cats[k]
for user_cat in sorted(prefs['user_categories'].keys()):
for user_cat in sorted(self.prefs.get('user_categories', {}).keys()):
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()):
if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
self.book_on_device_func = None
@ -311,6 +327,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
''' Return last modified time as a UTC datetime object'''
return utcfromtimestamp(os.stat(self.dbpath).st_mtime)
def check_if_modified(self):
if self.last_modified() > self.last_update_check:
self.refresh()
@ -581,6 +598,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def has_format(self, index, format, index_is_id=False):
return self.format_abspath(index, format, index_is_id) is not None
def format_last_modified(self, id_, fmt):
path = self.format_abspath(id_, fmt, index_is_id=True)
if path is not None:
return utcfromtimestamp(os.stat(path).st_mtime)
def format_abspath(self, index, format, index_is_id=False):
'Return absolute path to the ebook file of format `format`'
id = index if index_is_id else self.id(index)
@ -843,7 +865,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
categories['formats'].sort(key = lambda x:x.name)
#### Now do the user-defined categories. ####
user_categories = prefs['user_categories']
user_categories = self.prefs['user_categories']
# We want to use same node in the user category as in the source
# category. To do that, we need to find the original Tag node. There is
@ -880,8 +902,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
icon = None
if icon_map and 'search' in icon_map:
icon = icon_map['search']
for srch in saved_searches.names():
items.append(Tag(srch, tooltip=saved_searches.lookup(srch), icon=icon))
for srch in saved_searches().names():
items.append(Tag(srch, tooltip=saved_searches().lookup(srch), icon=icon))
if len(items):
if icon_map is not None:
icon_map['search'] = icon_map['search']

View File

@ -0,0 +1,49 @@
#!/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'
import json
from calibre.constants import preferred_encoding
from calibre.utils.config import to_json, from_json
class DBPrefs(dict):
def __init__(self, db):
dict.__init__(self)
self.db = db
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
val = self.raw_to_object(val)
dict.__setitem__(self, key, val)
def raw_to_object(self, raw):
if not isinstance(raw, unicode):
raw = raw.decode(preferred_encoding)
return json.loads(raw, object_hook=from_json)
def to_raw(self, val):
return json.dumps(val, indent=2, default=to_json)
def __getitem__(self, key):
return dict.__getitem__(self, key)
def __delitem__(self, key):
dict.__delitem__(self, key)
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
self.db.conn.commit()
def __setitem__(self, key, val):
raw = self.to_raw(val)
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
self.db.conn.execute('INSERT INTO preferences (key,val) VALUES (?,?)', (key,
raw))
self.db.conn.commit()
dict.__setitem__(self, key, val)
def set(self, key, val):
self.__setitem__(key, val)

View File

@ -387,3 +387,13 @@ class SchemaUpgrade(object):
self.conn.execute('UPDATE authors SET sort=author_to_author_sort(name)')
def upgrade_version_12(self):
'DB based preference store'
script = '''
DROP TABLE IF EXISTS preferences;
CREATE TABLE preferences(id INTEGER PRIMARY KEY,
key TEXT NON NULL,
val TEXT NON NULL,
UNIQUE(key));
'''
self.conn.executescript(script)

View File

@ -349,7 +349,7 @@ table of contents, check the :guilabel:`Do not add detected chapters` option.
If less than the :guilabel:`Chapter threshold` number of chapters were detected, |app| will then add any hyperlinks
it finds in the input document to the Table of Contents. This often works well many input documents include a
hyperlinked Table of Contents right at the start. The :guilabel:`Number fo links` option can be used to control
hyperlinked Table of Contents right at the start. The :guilabel:`Number of links` option can be used to control
this behavior. If set to zero, no links are added. If set to a number greater than zero, at most that number of links
is added.

View File

@ -9,7 +9,8 @@ Customizing |app|
|app| has a highly modular design. Various parts of it can be customized. You can learn how to create
*recipes* to add new sources of online content to |app| in the Section :ref:`news`. Here, you will learn,
first, how to use environment variables and *tweaks* to customize |app|'s behavior and then how to
first, how to use environment variables and *tweaks* to customize |app|'s behavior, and then how to
specify your own static resources like icons and templates to override the defaults and finally how to
use *plugins* to add funtionality to |app|.
.. contents::
@ -35,6 +36,20 @@ The default tweaks.py file is reproduced below
.. literalinclude:: ../../../resources/default_tweaks.py
Overriding icons, templates, etcetera
----------------------------------------
|app| allows you to override the static resources, like icons, templates, javascript, etc. with customized versions that you like.
All static resources are stored in the resources sub-folder of the calibre install location. On Windows, this is usually
:file:`C:\Program Files\Calibre2\resources`. On OS X, :file:`/Applications/calibre.app/Contents/Resources/resources/`. On linux, if you are using the binary installer
from the calibre website it will be :file:`/opt/calibre/resources`. These paths can change depending on where you choose to install |app|.
You should not change the files in this resources folder, as your changes will get overwritten the next time you update |app|. Instead, go to
:guilabel:`Preferences->Advanced` and click :guilabel:`Open calibre configuration directory`. In this configuration directory, create a sub-folder called resources and place the files you want to override in it. |app| will automatically use your custom file in preference to the builtin one the next time it is started.
For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is
:file:`resources/images/trash.svg`. Assuming you have an alternate icon in svg format called :file:`mytrash.svg` you would save it in the configuration directory as :file:`resources/images/trash.svg`. All the icons used by the calibre user interface are in :file:`resources/images` and its sub-folders.
A Hello World plugin
------------------------

View File

@ -281,7 +281,7 @@ Why doesn't |app| have a column for foo?
How do I move my |app| library from one computer to another?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking Preferences. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder.
Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking the calibre icon in the toolbar. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder.
Note that if you are transferring between different types of computers (for example Windows to OS X) then after doing the above you should also go to Preferences->Advanced and click the Check database integrity button. It will warn you about missing files, if any, which you should then transfer by hand.

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

View File

@ -66,7 +66,7 @@ and save it to a new file.
"""
import ctypes, sys, os
import ctypes, sys, os, glob
from ctypes import util
iswindows = 'win32' in sys.platform or 'win64' in sys.platform
isosx = 'darwin' in sys.platform
@ -85,7 +85,8 @@ elif iswindows:
_lib = flib if isfrozen else 'CORE_RL_wand_'
else:
if isfrozen:
_lib = os.path.join(sys.frozen_path, 'libMagickWand.so.2')
_lib = glob.glob(os.path.join(sys.frozen_path,
'libMagickWand.so.*'))[-1]
else:
_lib = util.find_library('MagickWand')
if _lib is None:

View File

@ -6,30 +6,16 @@ __docformat__ = 'restructuredtext en'
'''
Manage application-wide preferences.
'''
import os, re, cPickle, textwrap, traceback, plistlib, json, base64
import os, re, cPickle, textwrap, traceback, plistlib, json, base64, datetime
from copy import deepcopy
from functools import partial
from optparse import OptionParser as _OptionParser
from optparse import IndentedHelpFormatter
from calibre.constants import terminal_controller, iswindows, isosx, \
__appname__, __version__, __author__, plugins
from calibre.utils.lock import LockError, ExclusiveFile
from collections import defaultdict
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
elif iswindows:
if plugins['winutil'][0] is None:
raise Exception(plugins['winutil'][1])
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
if not os.access(config_dir, os.W_OK|os.X_OK):
config_dir = os.path.expanduser('~')
config_dir = os.path.join(config_dir, 'calibre')
elif isosx:
config_dir = os.path.expanduser('~/Library/Preferences/calibre')
else:
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
config_dir = os.path.join(bdir, 'calibre')
from calibre.constants import terminal_controller, config_dir, \
__appname__, __version__, __author__
from calibre.utils.lock import LockError, ExclusiveFile
plugin_dir = os.path.join(config_dir, 'plugins')
@ -632,27 +618,34 @@ class XMLConfig(dict):
f.truncate()
f.write(raw)
def to_json(obj):
if isinstance(obj, bytearray):
return {'__class__': 'bytearray',
'__value__': base64.standard_b64encode(bytes(obj))}
if isinstance(obj, datetime.datetime):
from calibre.utils.date import isoformat
return {'__class__': 'datetime.datetime',
'__value__': isoformat(obj, as_utc=True)}
raise TypeError(repr(obj) + ' is not JSON serializable')
def from_json(obj):
if '__class__' in obj:
if obj['__class__'] == 'bytearray':
return bytearray(base64.standard_b64decode(obj['__value__']))
if obj['__class__'] == 'datetime.datetime':
from calibre.utils.date import parse_date
return parse_date(obj['__value__'], assume_utc=True)
return obj
class JSONConfig(XMLConfig):
EXTENSION = '.json'
def to_json(self, obj):
if isinstance(obj, bytearray):
return {'__class__': 'bytearray',
'__value__': base64.standard_b64encode(bytes(obj))}
raise TypeError(repr(obj) + ' is not JSON serializable')
def from_json(self, obj):
if '__class__' in obj:
if obj['__class__'] == 'bytearray':
return bytearray(base64.standard_b64decode(obj['__value__']))
return obj
def raw_to_object(self, raw):
return json.loads(raw.decode('utf-8'), object_hook=self.from_json)
return json.loads(raw.decode('utf-8'), object_hook=from_json)
def to_raw(self):
return json.dumps(self, indent=2, default=self.to_json)
return json.dumps(self, indent=2, default=to_json)
def __getitem__(self, key):
return dict.__getitem__(self, key)

View File

@ -42,6 +42,8 @@ def get_lang():
lang = match.group()
if lang == 'zh':
lang = 'zh_CN'
if lang is None:
lang = 'en'
return lang
def messages_path(lang):

View File

@ -9,26 +9,54 @@ __docformat__ = 'restructuredtext en'
import __builtin__, sys, os
_dev_path = os.environ.get('CALIBRE_DEVELOP_FROM', None)
if _dev_path is not None:
_dev_path = os.path.join(os.path.abspath(os.path.dirname(_dev_path)), 'resources')
if not os.path.exists(_dev_path):
_dev_path = None
from calibre import config_dir
_path_cache = {}
class PathResolver(object):
def __init__(self):
self.locations = [sys.resources_location]
self.cache = {}
def suitable(path):
try:
return os.path.exists(path) and os.path.isdir(path) and \
os.listdir(path)
except:
pass
return False
dev_path = os.environ.get('CALIBRE_DEVELOP_FROM', None)
if dev_path is not None:
dev_path = os.path.join(os.path.abspath(
os.path.dirname(dev_path)), 'resources')
if suitable(dev_path):
self.locations.insert(0, dev_path)
user_path = os.path.join(config_dir, 'resources')
if suitable(user_path):
self.locations.insert(0, user_path)
def __call__(self, path):
path = path.replace(os.sep, '/')
ans = self.cache.get(path, None)
if ans is None:
for base in self.locations:
fpath = os.path.join(base, *path.split('/'))
if os.path.exists(fpath):
ans = fpath
break
if ans is None:
ans = os.path.join(self.locations[0], *path.split('/'))
self.cache[path] = ans
return ans
_resolver = PathResolver()
def get_path(path, data=False):
global _dev_path
path = path.replace(os.sep, '/')
base = sys.resources_location
if _dev_path is not None:
if path in _path_cache:
return _path_cache[path]
if os.path.exists(os.path.join(_dev_path, *path.split('/'))):
base = _dev_path
fpath = os.path.join(base, *path.split('/'))
if _dev_path is not None:
_path_cache[path] = fpath
fpath = _resolver(path)
if data:
return open(fpath, 'rb').read()
return fpath

View File

@ -18,10 +18,11 @@ If this module is run, it will perform a series of unit tests.
import sys, string, operator
from calibre.utils.pyparsing import Keyword, Group, Forward, CharsNotIn, Suppress, \
OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch, ParseException
from calibre.utils.pyparsing import CaselessKeyword, Group, Forward, CharsNotIn, Suppress, \
OneOrMore, MatchFirst, CaselessLiteral, Optional, NoMatch, ParseException
from calibre.constants import preferred_encoding
from calibre.utils.config import prefs
'''
This class manages access to the preference holding the saved search queries.
@ -32,9 +33,13 @@ class SavedSearchQueries(object):
queries = {}
opt_name = ''
def __init__(self, _opt_name):
def __init__(self, db, _opt_name):
self.opt_name = _opt_name;
self.queries = prefs[self.opt_name]
self.db = db
if db is not None:
self.queries = db.prefs.get(self.opt_name, {})
else:
self.queries = {}
def force_unicode(self, x):
if not isinstance(x, unicode):
@ -43,20 +48,20 @@ class SavedSearchQueries(object):
def add(self, name, value):
self.queries[self.force_unicode(name)] = self.force_unicode(value).strip()
prefs[self.opt_name] = self.queries
self.db.prefs[self.opt_name] = self.queries
def lookup(self, name):
return self.queries.get(self.force_unicode(name), None)
def delete(self, name):
self.queries.pop(self.force_unicode(name), False)
prefs[self.opt_name] = self.queries
self.db.prefs[self.opt_name] = self.queries
def rename(self, old_name, new_name):
self.queries[self.force_unicode(new_name)] = \
self.queries.get(self.force_unicode(old_name), None)
self.queries.pop(self.force_unicode(old_name), False)
prefs[self.opt_name] = self.queries
self.db.prefs[self.opt_name] = self.queries
def names(self):
return sorted(self.queries.keys(),
@ -66,8 +71,15 @@ class SavedSearchQueries(object):
Create a global instance of the saved searches. It is global so that the searches
are common across all instances of the parser (devices, library, etc).
'''
saved_searches = SavedSearchQueries('saved_searches')
ss = SavedSearchQueries(None, None)
def set_saved_searches(db, opt_name):
global ss
ss = SavedSearchQueries(db, opt_name)
def saved_searches():
global ss
return ss
class SearchQueryParser(object):
'''
@ -139,18 +151,19 @@ class SearchQueryParser(object):
Not = Forward()
Not << (Group(
Suppress(Keyword("not", caseless=True)) + Not
Suppress(CaselessKeyword("not")) + Not
).setResultsName("not") | Parenthesis)
And = Forward()
And << (Group(
Not + Suppress(Keyword("and", caseless=True)) + And
Not + Suppress(CaselessKeyword("and")) + And
).setResultsName("and") | Group(
Not + OneOrMore(~oneOf("and or", caseless=True) + And)
Not + OneOrMore(~MatchFirst(list(map(CaselessKeyword,
('and', 'or')))) + And)
).setResultsName("and") | Not)
Or << (Group(
And + Suppress(Keyword("or", caseless=True)) + Or
And + Suppress(CaselessKeyword("or")) + Or
).setResultsName("or") | And)
if test:
@ -158,8 +171,6 @@ class SearchQueryParser(object):
self._tests_failed = bool(failed)
self._parser = Or
#self._parser.setDebug(True)
#self.parse('(tolstoy)')
self._parser.setDebug(False)
@ -209,7 +220,7 @@ class SearchQueryParser(object):
raise ParseException(query, len(query), 'undefined saved search', self)
if self.recurse_level > 5:
self.searches_seen.add(query)
return self._parse(saved_searches.lookup(query))
return self._parse(saved_searches().lookup(query))
except: # convert all exceptions (e.g., missing key) to a parse error
raise ParseException(query, len(query), 'undefined saved search', self)
return self.get_matches(location, query)
@ -283,7 +294,7 @@ class Tester(SearchQueryParser):
28: [u"Kushiel's Scion", u'Jacqueline Carey', None, u'lrf,rar'],
29: [u'Underworld', u'Don DeLillo', None, u'lrf,rar'],
30: [u'Genghis Khan and The Making of the Modern World',
u'Jack Weatherford',
u'Jack Weatherford Orc',
u'Three Rivers Press',
u'lrf,zip'],
31: [u'The Best and the Brightest',
@ -535,6 +546,7 @@ class Tester(SearchQueryParser):
'london:thames': set([13]),
'publisher:london:thames': set([13]),
'"(1977)"': set([13]),
'jack weatherford orc': set([30]),
}
fields = {'title':0, 'author':1, 'publisher':2, 'tag':3}
@ -574,7 +586,10 @@ class Tester(SearchQueryParser):
def main(args=sys.argv):
tester = Tester(test=True)
tester = Tester(['authors', 'author', 'series', 'formats', 'format',
'publisher', 'rating', 'tags', 'tag', 'comments', 'comment', 'cover',
'isbn', 'ondevice', 'pubdate', 'size', 'date', 'title', u'#read',
'all', 'search'], test=True)
failed = tester.run_tests()
if tester._tests_failed or failed:
print '>>>>>>>>>>>>>> Tests Failed <<<<<<<<<<<<<<<'