'
__docformat__ = 'restructuredtext en'
-import traceback, sys, textwrap, re
+import traceback, sys, textwrap, re, urllib2
from threading import Thread
-from calibre import prints
+from calibre import prints, browser
from calibre.utils.config import OptionParser
from calibre.utils.logging import default_log
from calibre.customize import Plugin
+from calibre.ebooks.metadata.library_thing import OPENLIBRARY
metadata_config = None
-class MetadataSource(Plugin):
+class MetadataSource(Plugin): # {{{
author = 'Kovid Goyal'
@@ -130,7 +131,9 @@ class MetadataSource(Plugin):
def customization_help(self):
return 'This plugin can only be customized using the GUI'
-class GoogleBooks(MetadataSource):
+ # }}}
+
+class GoogleBooks(MetadataSource): # {{{
name = 'Google Books'
description = _('Downloads metadata from Google Books')
@@ -145,8 +148,9 @@ class GoogleBooks(MetadataSource):
self.exception = e
self.tb = traceback.format_exc()
+ # }}}
-class ISBNDB(MetadataSource):
+class ISBNDB(MetadataSource): # {{{
name = 'IsbnDB'
description = _('Downloads metadata from isbndb.com')
@@ -181,7 +185,9 @@ class ISBNDB(MetadataSource):
'and enter your access key below.')
return ''+ans%('', '')
-class Amazon(MetadataSource):
+ # }}}
+
+class Amazon(MetadataSource): # {{{
name = 'Amazon'
metadata_type = 'social'
@@ -198,37 +204,27 @@ class Amazon(MetadataSource):
self.exception = e
self.tb = traceback.format_exc()
-class LibraryThing(MetadataSource):
+ # }}}
+
+class LibraryThing(MetadataSource): # {{{
name = 'LibraryThing'
metadata_type = 'social'
- description = _('Downloads series information from librarything.com')
+ description = _('Downloads series/tags/rating information from librarything.com')
def fetch(self):
if not self.isbn:
return
- from calibre import browser
- from calibre.ebooks.metadata import MetaInformation
- import json
- br = browser()
+ from calibre.ebooks.metadata.library_thing import get_social_metadata
try:
- raw = br.open(
- 'http://status.calibre-ebook.com/library_thing/metadata/'+self.isbn
- ).read()
- data = json.loads(raw)
- if not data:
- return
- if 'error' in data:
- raise Exception(data['error'])
- if 'series' in data and 'series_index' in data:
- mi = MetaInformation(self.title, [])
- mi.series = data['series']
- mi.series_index = data['series_index']
- self.results = mi
+ self.results = get_social_metadata(self.title, self.book_author,
+ self.publisher, self.isbn)
except Exception, e:
self.exception = e
self.tb = traceback.format_exc()
+ # }}}
+
def result_index(source, result):
if not result.isbn:
@@ -268,6 +264,31 @@ class MetadataSources(object):
for s in self.sources:
s.join()
+def filter_metadata_results(item):
+ keywords = ["audio", "tape", "cassette", "abridged", "playaway"]
+ for keyword in keywords:
+ if item.publisher and keyword in item.publisher.lower():
+ return False
+ return True
+
+class HeadRequest(urllib2.Request):
+ def get_method(self):
+ return "HEAD"
+
+def do_cover_check(item):
+ opener = browser()
+ item.has_cover = False
+ try:
+ opener.open(HeadRequest(OPENLIBRARY%item.isbn), timeout=5)
+ item.has_cover = True
+ except:
+ pass # Cover not found
+
+def check_for_covers(items):
+ threads = [Thread(target=do_cover_check, args=(item,)) for item in items]
+ for t in threads: t.start()
+ for t in threads: t.join()
+
def search(title=None, author=None, publisher=None, isbn=None, isbndb_key=None,
verbose=0):
assert not(title is None and author is None and publisher is None and \
@@ -285,10 +306,60 @@ def search(title=None, author=None, publisher=None, isbn=None, isbndb_key=None,
for fetcher in fetchers[1:]:
merge_results(results, fetcher.results)
- results = sorted(results, cmp=lambda x, y : cmp(
- (x.comments.strip() if x.comments else ''),
- (y.comments.strip() if y.comments else '')
- ), reverse=True)
+ results = list(filter(filter_metadata_results, results))
+
+ check_for_covers(results)
+
+ words = ("the", "a", "an", "of", "and")
+ prefix_pat = re.compile(r'^(%s)\s+'%("|".join(words)))
+ trailing_paren_pat = re.compile(r'\(.*\)$')
+ whitespace_pat = re.compile(r'\s+')
+
+ def sort_func(x, y):
+
+ def cleanup_title(s):
+ s = s.strip().lower()
+ s = prefix_pat.sub(' ', s)
+ s = trailing_paren_pat.sub('', s)
+ s = whitespace_pat.sub(' ', s)
+ return s.strip()
+
+ t = cleanup_title(title)
+ x_title = cleanup_title(x.title)
+ y_title = cleanup_title(y.title)
+
+ # prefer titles that start with the search title
+ tx = cmp(t, x_title)
+ ty = cmp(t, y_title)
+ result = 0 if abs(tx) == abs(ty) else abs(tx) - abs(ty)
+
+ # then prefer titles that have a cover image
+ if result == 0:
+ result = -cmp(x.has_cover, y.has_cover)
+
+ # then prefer titles with the longest comment, with in 10%
+ if result == 0:
+ cx = len(x.comments.strip() if x.comments else '')
+ cy = len(y.comments.strip() if y.comments else '')
+ t = (cx + cy) / 20
+ result = cy - cx
+ if abs(result) < t:
+ result = 0
+
+ return result
+
+ results = sorted(results, cmp=sort_func)
+
+ # if for some reason there is no comment in the top selection, go looking for one
+ if len(results) > 1:
+ if not results[0].comments or len(results[0].comments) == 0:
+ for r in results[1:]:
+ if title.lower() == r.title[:len(title)].lower() and r.comments and len(r.comments):
+ results[0].comments = r.comments
+ break
+
+ # for r in results:
+ # print "{0:14.14} {1:30.30} {2:20.20} {3:6} {4}".format(r.isbn, r.title, r.publisher, len(r.comments if r.comments else ''), r.has_cover)
return results, [(x.name, x.exception, x.tb) for x in fetchers]
diff --git a/src/calibre/ebooks/metadata/library_thing.py b/src/calibre/ebooks/metadata/library_thing.py
index d10d80bc61..3a78204e8e 100644
--- a/src/calibre/ebooks/metadata/library_thing.py
+++ b/src/calibre/ebooks/metadata/library_thing.py
@@ -6,10 +6,11 @@ Fetch cover from LibraryThing.com based on ISBN number.
import sys, socket, os, re
-from calibre import browser as _browser
+from lxml import html
+
+from calibre import browser, prints
from calibre.utils.config import OptionParser
from calibre.ebooks.BeautifulSoup import BeautifulSoup
-browser = None
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
@@ -22,31 +23,28 @@ class ISBNNotFound(LibraryThingError):
class ServerBusy(LibraryThingError):
pass
-def login(username, password, force=True):
- global browser
- if browser is not None and not force:
- return
- browser = _browser()
- browser.open('http://www.librarything.com')
- browser.select_form('signup')
- browser['formusername'] = username
- browser['formpassword'] = password
- browser.submit()
+def login(br, username, password, force=True):
+ br.open('http://www.librarything.com')
+ br.select_form('signup')
+ br['formusername'] = username
+ br['formpassword'] = password
+ br.submit()
def cover_from_isbn(isbn, timeout=5., username=None, password=None):
- global browser
- if browser is None:
- browser = _browser()
src = None
+ br = browser()
try:
- return browser.open(OPENLIBRARY%isbn, timeout=timeout).read(), 'jpg'
+ return br.open(OPENLIBRARY%isbn, timeout=timeout).read(), 'jpg'
except:
pass # Cover not found
if username and password:
- login(username, password, force=False)
+ try:
+ login(br, username, password, force=False)
+ except:
+ pass
try:
- src = browser.open('http://www.librarything.com/isbn/'+isbn,
+ src = br.open_novisit('http://www.librarything.com/isbn/'+isbn,
timeout=timeout).read().decode('utf-8', 'replace')
except Exception, err:
if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
@@ -63,7 +61,7 @@ def cover_from_isbn(isbn, timeout=5., username=None, password=None):
if url is None:
raise LibraryThingError(_('LibraryThing.com server error. Try again later.'))
url = re.sub(r'_S[XY]\d+', '', url['src'])
- cover_data = browser.open(url).read()
+ cover_data = br.open_novisit(url).read()
return cover_data, url.rpartition('.')[-1]
def option_parser():
@@ -71,7 +69,7 @@ def option_parser():
_('''
%prog [options] ISBN
-Fetch a cover image for the book identified by ISBN from LibraryThing.com
+Fetch a cover image/social metadata for the book identified by ISBN from LibraryThing.com
'''))
parser.add_option('-u', '--username', default=None,
help='Username for LibraryThing.com')
@@ -79,6 +77,61 @@ Fetch a cover image for the book identified by ISBN from LibraryThing.com
help='Password for LibraryThing.com')
return parser
+def get_social_metadata(title, authors, publisher, isbn, username=None,
+ password=None):
+ from calibre.ebooks.metadata import MetaInformation
+ mi = MetaInformation(title, authors)
+ if isbn:
+ br = browser()
+ if username and password:
+ try:
+ login(br, username, password, force=False)
+ except:
+ pass
+
+ raw = br.open_novisit('http://www.librarything.com/isbn/'
+ +isbn).read()
+ if not raw:
+ return mi
+ root = html.fromstring(raw)
+ h1 = root.xpath('//div[@class="headsummary"]/h1')
+ if h1 and not mi.title:
+ mi.title = html.tostring(h1[0], method='text', encoding=unicode)
+ h2 = root.xpath('//div[@class="headsummary"]/h2/a')
+ if h2 and not mi.authors:
+ mi.authors = [html.tostring(x, method='text', encoding=unicode) for
+ x in h2]
+ h3 = root.xpath('//div[@class="headsummary"]/h3/a')
+ if h3:
+ match = None
+ for h in h3:
+ series = html.tostring(h, method='text', encoding=unicode)
+ match = re.search(r'(.+) \((.+)\)', series)
+ if match is not None:
+ break
+ if match is not None:
+ mi.series = match.group(1).strip()
+ match = re.search(r'[0-9.]+', match.group(2))
+ si = 1.0
+ if match is not None:
+ si = float(match.group())
+ mi.series_index = si
+ tags = root.xpath('//div[@class="tags"]/span[@class="tag"]/a')
+ if tags:
+ mi.tags = [html.tostring(x, method='text', encoding=unicode) for x
+ in tags]
+ span = root.xpath(
+ '//table[@class="wsltable"]/tr[@class="wslcontent"]/td[4]//span')
+ if span:
+ raw = html.tostring(span[0], method='text', encoding=unicode)
+ match = re.search(r'([0-9.]+)', raw)
+ if match is not None:
+ rating = float(match.group())
+ if rating > 0 and rating <= 5:
+ mi.rating = rating
+ return mi
+
+
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
@@ -86,6 +139,8 @@ def main(args=sys.argv):
parser.print_help()
return 1
isbn = args[1]
+ mi = get_social_metadata('', [], '', isbn)
+ prints(mi)
cover_data, ext = cover_from_isbn(isbn, username=opts.username,
password=opts.password)
if not ext:
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 46924cad1f..579398d3b0 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -18,7 +18,7 @@ from calibre.constants import __appname__, __version__, filesystem_encoding
from calibre.ebooks.metadata.toc import TOC
from calibre.ebooks.metadata import MetaInformation, string_to_authors
from calibre.utils.date import parse_date, isoformat
-
+from calibre.utils.localization import get_lang
class Resource(object):
'''
@@ -1069,7 +1069,7 @@ class OPFCreator(MetaInformation):
dc_attrs={'id':__appname__+'_id'}))
if getattr(self, 'pubdate', None) is not None:
a(DC_ELEM('date', self.pubdate.isoformat()))
- a(DC_ELEM('language', self.language if self.language else 'UND'))
+ a(DC_ELEM('language', self.language if self.language else get_lang()))
if self.comments:
a(DC_ELEM('description', self.comments))
if self.publisher:
@@ -1184,7 +1184,6 @@ def metadata_to_opf(mi, as_string=True):
factory(DC('contributor'), mi.book_producer, __appname__, 'bkp')
if hasattr(mi.pubdate, 'isoformat'):
factory(DC('date'), isoformat(mi.pubdate))
- factory(DC('language'), mi.language)
if mi.category:
factory(DC('type'), mi.category)
if mi.comments:
@@ -1195,6 +1194,7 @@ def metadata_to_opf(mi, as_string=True):
factory(DC('identifier'), mi.isbn, scheme='ISBN')
if mi.rights:
factory(DC('rights'), mi.rights)
+ factory(DC('language'), mi.language if mi.language and mi.language.lower() != 'und' else get_lang())
if mi.tags:
for tag in mi.tags:
factory(DC('subject'), tag)
diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py
index 76e2cef3bb..54549ac415 100644
--- a/src/calibre/ebooks/oeb/base.py
+++ b/src/calibre/ebooks/oeb/base.py
@@ -17,6 +17,7 @@ from urlparse import urljoin
from lxml import etree, html
from cssutils import CSSParser
+from cssutils.css import CSSRule
import calibre
from calibre.constants import filesystem_encoding
@@ -762,6 +763,7 @@ class Manifest(object):
self.href = self.path = urlnormalize(href)
self.media_type = media_type
self.fallback = fallback
+ self.override_css_fetch = None
self.spine_position = None
self.linear = True
if loader is None and data is None:
@@ -982,15 +984,40 @@ class Manifest(object):
def _parse_css(self, data):
+
+ def get_style_rules_from_import(import_rule):
+ ans = []
+ if not import_rule.styleSheet:
+ return ans
+ rules = import_rule.styleSheet.cssRules
+ for rule in rules:
+ if rule.type == CSSRule.IMPORT_RULE:
+ ans.extend(get_style_rules_from_import(rule))
+ elif rule.type in (CSSRule.FONT_FACE_RULE,
+ CSSRule.STYLE_RULE):
+ ans.append(rule)
+ return ans
+
self.oeb.log.debug('Parsing', self.href, '...')
data = self.oeb.decode(data)
- data = self.oeb.css_preprocessor(data)
- data = XHTML_CSS_NAMESPACE + data
+ data = self.oeb.css_preprocessor(data, add_namespace=True)
parser = CSSParser(loglevel=logging.WARNING,
- fetcher=self._fetch_css,
+ fetcher=self.override_css_fetch or self._fetch_css,
log=_css_logger)
data = parser.parseString(data, href=self.href)
data.namespaces['h'] = XHTML_NS
+ import_rules = list(data.cssRules.rulesOfType(CSSRule.IMPORT_RULE))
+ rules_to_append = []
+ insert_index = None
+ for r in data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
+ insert_index = data.cssRules.index(r)
+ break
+ for rule in import_rules:
+ rules_to_append.extend(get_style_rules_from_import(rule))
+ for r in reversed(rules_to_append):
+ data.insertRule(r, index=insert_index)
+ for rule in import_rules:
+ data.deleteRule(rule)
return data
def _fetch_css(self, path):
diff --git a/src/calibre/ebooks/oeb/iterator.py b/src/calibre/ebooks/oeb/iterator.py
index 3fdd6aaf99..7f56cb4d2d 100644
--- a/src/calibre/ebooks/oeb/iterator.py
+++ b/src/calibre/ebooks/oeb/iterator.py
@@ -139,11 +139,18 @@ class EbookIterator(object):
if id != -1:
families = [unicode(f) for f in QFontDatabase.applicationFontFamilies(id)]
if family:
- family = family.group(1).strip().replace('"', '')
- bad_map[family] = families[0]
- if family not in families:
+ family = family.group(1)
+ specified_families = [x.strip().replace('"',
+ '').replace("'", '') for x in family.split(',')]
+ aliasing_ok = False
+ for f in specified_families:
+ bad_map[f] = families[0]
+ if not aliasing_ok and f in families:
+ aliasing_ok = True
+
+ if not aliasing_ok:
prints('WARNING: Family aliasing not fully supported.')
- prints('\tDeclared family: %s not in actual families: %s'
+ prints('\tDeclared family: %r not in actual families: %r'
% (family, families))
else:
prints('Loaded embedded font:', repr(family))
diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py
index 0637dddfb6..3c84252ff4 100644
--- a/src/calibre/ebooks/oeb/stylizer.py
+++ b/src/calibre/ebooks/oeb/stylizer.py
@@ -126,6 +126,13 @@ class Stylizer(object):
head = head[0]
else:
head = []
+
+ # Add cssutils parsing profiles from output_profile
+ for profile in self.opts.output_profile.extra_css_modules:
+ cssutils.profile.addProfile(profile['name'],
+ profile['props'],
+ profile['macros'])
+
parser = cssutils.CSSParser(fetcher=self._fetch_css_file,
log=logging.getLogger('calibre.css'))
self.font_face_rules = []
diff --git a/src/calibre/gui2/actions.py b/src/calibre/gui2/actions.py
index f838e9c1fe..43a657ae67 100644
--- a/src/calibre/gui2/actions.py
+++ b/src/calibre/gui2/actions.py
@@ -21,6 +21,7 @@ from calibre.utils.filenames import ascii_filename
from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
+from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
fetch_scheduled_recipe, generate_catalog
from calibre.constants import preferred_encoding, filesystem_encoding, \
@@ -176,7 +177,8 @@ class AnnotationsAction(object): # {{{
def mark_book_as_read(self,id):
read_tag = gprefs.get('catalog_epub_mobi_read_tag')
- self.db.set_tags(id, [read_tag], append=True)
+ if read_tag:
+ self.db.set_tags(id, [read_tag], append=True)
def canceled(self):
self.pd.hide()
@@ -830,6 +832,23 @@ class EditMetadataAction(object): # {{{
db.set_metadata(dest_id, dest_mi, ignore_errors=False)
# }}}
+ def edit_device_collections(self, view, oncard=None):
+ model = view.model()
+ result = model.get_collections_with_ids()
+ compare = (lambda x,y:cmp(x.lower(), y.lower()))
+ d = TagListEditor(self, tag_to_match=None, data=result, compare=compare)
+ d.exec_()
+ if d.result() == d.Accepted:
+ to_rename = d.to_rename # dict of new text to old ids
+ to_delete = d.to_delete # list of ids
+ for text in to_rename:
+ for old_id in to_rename[text]:
+ model.rename_collection(old_id, new_name=unicode(text))
+ for item in to_delete:
+ model.delete_collection_using_id(item)
+ self.upload_collections(model.db, view=view, oncard=oncard)
+ view.reset()
+
# }}}
class SaveToDiskAction(object): # {{{
diff --git a/src/calibre/gui2/convert/txt_input.ui b/src/calibre/gui2/convert/txt_input.ui
index 5a9527ebc5..186783c277 100644
--- a/src/calibre/gui2/convert/txt_input.ui
+++ b/src/calibre/gui2/convert/txt_input.ui
@@ -43,6 +43,9 @@
true
+
+ true
+
-
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 90ac7dbbaf..6bb481ddec 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -5,7 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import sys
+import re, sys
from functools import partial
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
@@ -162,7 +162,6 @@ class DateTime(Base):
val = qt_to_dt(val)
return val
-
class Comments(Base):
def setup_ui(self, parent):
@@ -199,11 +198,7 @@ class Text(Base):
w = EnComboBox(parent)
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
w.setMinimumContentsLength(25)
-
-
-
- self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
- w]
+ self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
def initialize(self, book_id):
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
@@ -222,7 +217,6 @@ class Text(Base):
if idx is not None:
self.widgets[1].setCurrentIndex(idx)
-
def setter(self, val):
if self.col_metadata['is_multiple']:
if not val:
@@ -241,6 +235,58 @@ class Text(Base):
val = None
return val
+class Series(Base):
+
+ def setup_ui(self, parent):
+ values = self.all_values = list(self.db.all_custom(num=self.col_id))
+ values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower()))
+ w = EnComboBox(parent)
+ w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
+ w.setMinimumContentsLength(25)
+ self.name_widget = w
+ self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
+
+ self.widgets.append(QLabel('&'+self.col_metadata['name']+_(' index:'), parent))
+ w = QDoubleSpinBox(parent)
+ w.setRange(-100., float(sys.maxint))
+ w.setDecimals(2)
+ w.setSpecialValueText(_('Undefined'))
+ w.setSingleStep(1)
+ self.idx_widget=w
+ self.widgets.append(w)
+
+ def initialize(self, book_id):
+ val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
+ s_index = self.db.get_custom_extra(book_id, num=self.col_id, index_is_id=True)
+ if s_index is None:
+ s_index = 0.0
+ self.idx_widget.setValue(s_index)
+ self.initial_index = s_index
+ self.initial_val = val
+ val = self.normalize_db_val(val)
+ idx = None
+ for i, c in enumerate(self.all_values):
+ if c == val:
+ idx = i
+ self.name_widget.addItem(c)
+ self.name_widget.setEditText('')
+ if idx is not None:
+ self.widgets[1].setCurrentIndex(idx)
+
+ def commit(self, book_id, notify=False):
+ val = unicode(self.name_widget.currentText()).strip()
+ val = self.normalize_ui_val(val)
+ s_index = self.idx_widget.value()
+ if val != self.initial_val or s_index != self.initial_index:
+ if s_index == 0.0:
+ if tweaks['series_index_auto_increment'] == 'next':
+ s_index = self.db.get_next_cc_series_num_for(val,
+ num=self.col_id)
+ else:
+ s_index = None
+ self.db.set_custom(book_id, val, extra=s_index,
+ num=self.col_id, notify=notify)
+
widgets = {
'bool' : Bool,
'rating' : Rating,
@@ -249,6 +295,7 @@ widgets = {
'datetime': DateTime,
'text' : Text,
'comments': Comments,
+ 'series': Series,
}
def field_sort(y, z, x=None):
@@ -257,35 +304,63 @@ def field_sort(y, z, x=None):
n2 = 'zzzzz' if m2['datatype'] == 'comments' else m2['name']
return cmp(n1.lower(), n2.lower())
-def populate_single_metadata_page(left, right, db, book_id, parent=None):
+def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, parent=None):
+ def widget_factory(type, col):
+ if bulk:
+ w = bulk_widgets[type](db, col, parent)
+ else:
+ w = widgets[type](db, col, parent)
+ w.initialize(book_id)
+ return w
x = db.custom_column_num_map
cols = list(x)
cols.sort(cmp=partial(field_sort, x=x))
+ count_non_comment = len([c for c in cols if x[c]['datatype'] != 'comments'])
+
+ layout.setColumnStretch(1, 10)
+ if two_column:
+ turnover_point = (count_non_comment+1)/2
+ layout.setColumnStretch(3, 10)
+ else:
+ # Avoid problems with multi-line widgets
+ turnover_point = count_non_comment + 1000
ans = []
- for i, col in enumerate(cols):
- w = widgets[x[col]['datatype']](db, col, parent)
+ column = row = 0
+ for col in cols:
+ dt = x[col]['datatype']
+ if dt == 'comments':
+ continue
+ w = widget_factory(dt, col)
ans.append(w)
- w.initialize(book_id)
- layout = left if i%2 == 0 else right
- row = layout.rowCount()
- if len(w.widgets) == 1:
- layout.addWidget(w.widgets[0], row, 0, 1, -1)
- else:
- w.widgets[0].setBuddy(w.widgets[1])
- for c, widget in enumerate(w.widgets):
- layout.addWidget(widget, row, c)
+ for c in range(0, len(w.widgets), 2):
+ w.widgets[c].setBuddy(w.widgets[c+1])
+ layout.addWidget(w.widgets[c], row, column)
+ layout.addWidget(w.widgets[c+1], row, column+1)
+ row += 1
+ if row >= turnover_point:
+ column += 2
+ turnover_point = count_non_comment + 1000
+ row = 0
+ if not bulk: # Add the comments fields
+ column = 0
+ for col in cols:
+ dt = x[col]['datatype']
+ if dt != 'comments':
+ continue
+ w = widget_factory(dt, col)
+ ans.append(w)
+ layout.addWidget(w.widgets[0], row, column, 1, 2)
+ if two_column and column == 0:
+ column = 2
+ continue
+ column = 0
+ row += 1
items = []
if len(ans) > 0:
items.append(QSpacerItem(10, 10, QSizePolicy.Minimum,
QSizePolicy.Expanding))
- left.addItem(items[-1], left.rowCount(), 0, 1, 1)
- left.setRowStretch(left.rowCount()-1, 100)
- if len(ans) > 1:
- items.append(QSpacerItem(10, 100, QSizePolicy.Minimum,
- QSizePolicy.Expanding))
- right.addItem(items[-1], left.rowCount(), 0, 1, 1)
- right.setRowStretch(right.rowCount()-1, 100)
-
+ layout.addItem(items[-1], layout.rowCount(), 0, 1, 1)
+ layout.setRowStretch(layout.rowCount()-1, 100)
return ans, items
class BulkBase(Base):
@@ -342,6 +417,47 @@ class BulkRating(BulkBase, Rating):
class BulkDateTime(BulkBase, DateTime):
pass
+class BulkSeries(BulkBase):
+ def setup_ui(self, parent):
+ values = self.all_values = list(self.db.all_custom(num=self.col_id))
+ values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower()))
+ w = EnComboBox(parent)
+ w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
+ w.setMinimumContentsLength(25)
+ self.name_widget = w
+ self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
+
+ self.widgets.append(QLabel(_('Automatically number books in this series'), parent))
+ self.idx_widget=QCheckBox(parent)
+ self.widgets.append(self.idx_widget)
+
+ def initialize(self, book_id):
+ self.idx_widget.setChecked(False)
+ for c in self.all_values:
+ self.name_widget.addItem(c)
+ self.name_widget.setEditText('')
+
+ def commit(self, book_ids, notify=False):
+ val = unicode(self.name_widget.currentText()).strip()
+ val = self.normalize_ui_val(val)
+ update_indices = self.idx_widget.checkState()
+ if val != '':
+ for book_id in book_ids:
+ if update_indices:
+ if tweaks['series_index_auto_increment'] == 'next':
+ s_index = self.db.get_next_cc_series_num_for\
+ (val, num=self.col_id)
+ else:
+ s_index = 1.0
+ else:
+ s_index = self.db.get_custom_extra(book_id, num=self.col_id,
+ index_is_id=True)
+ self.db.set_custom(book_id, val, extra=s_index,
+ num=self.col_id, notify=notify)
+
+ def process_each_book(self):
+ return True
+
class RemoveTags(QWidget):
def __init__(self, parent, values):
@@ -431,35 +547,5 @@ bulk_widgets = {
'float': BulkFloat,
'datetime': BulkDateTime,
'text' : BulkText,
-}
-
-def populate_bulk_metadata_page(layout, db, book_ids, parent=None):
- x = db.custom_column_num_map
- cols = list(x)
- cols.sort(cmp=partial(field_sort, x=x))
- ans = []
- for i, col in enumerate(cols):
- dt = x[col]['datatype']
- if dt == 'comments':
- continue
- w = bulk_widgets[dt](db, col, parent)
- ans.append(w)
- w.initialize(book_ids)
- row = layout.rowCount()
- if len(w.widgets) == 1:
- layout.addWidget(w.widgets[0], row, 0, 1, -1)
- else:
- for c in range(0, len(w.widgets), 2):
- w.widgets[c].setBuddy(w.widgets[c+1])
- layout.addWidget(w.widgets[c], row, 0)
- layout.addWidget(w.widgets[c+1], row, 1)
- row += 1
- items = []
- if len(ans) > 0:
- items.append(QSpacerItem(10, 10, QSizePolicy.Minimum,
- QSizePolicy.Expanding))
- layout.addItem(items[-1], layout.rowCount(), 0, 1, 1)
- layout.setRowStretch(layout.rowCount()-1, 100)
-
- return ans, items
-
+ 'series': BulkSeries,
+}
\ No newline at end of file
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index 33191d1773..6be50cf293 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -10,7 +10,7 @@ from functools import partial
from binascii import unhexlify
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
- Qt, pyqtSignal, QColor, QPainter
+ Qt, pyqtSignal, QColor, QPainter, QDialog
from PyQt4.QtSvg import QSvgRenderer
from calibre.customize.ui import available_input_formats, available_output_formats, \
@@ -294,6 +294,11 @@ class DeviceManager(Thread): # {{{
return self.create_job(self._sync_booklists, done, args=[booklists],
description=_('Send metadata to device'))
+ def upload_collections(self, done, booklist, on_card):
+ return self.create_job(booklist.rebuild_collections, done,
+ args=[booklist, on_card],
+ description=_('Send collections to device'))
+
def _upload_books(self, files, names, on_card=None, metadata=None):
'''Upload books to device: '''
return self.device.upload_books(files, names, on_card,
@@ -814,7 +819,8 @@ class DeviceMixin(object): # {{{
if specific:
d = ChooseFormatDialog(self, _('Choose format to send to device'),
self.device_manager.device.settings().format_map)
- d.exec_()
+ if d.exec_() != QDialog.Accepted:
+ return
if d.format():
fmt = d.format().lower()
dest, sub_dest = dest.split(':')
@@ -1227,6 +1233,19 @@ class DeviceMixin(object): # {{{
return
cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
+ # reset the views so that up-to-date info is shown. These need to be
+ # here because the sony driver updates collections in sync_booklists
+ self.memory_view.reset()
+ self.card_a_view.reset()
+ self.card_b_view.reset()
+
+ def _upload_collections(self, job):
+ if job.failed:
+ self.device_job_exception(job)
+
+ def upload_collections(self, booklist, view=None, oncard=None):
+ return self.device_manager.upload_collections(self._upload_collections,
+ booklist, oncard)
def upload_books(self, files, names, metadata, on_card=None, memory=None):
'''
diff --git a/src/calibre/gui2/dialogs/choose_format.ui b/src/calibre/gui2/dialogs/choose_format.ui
index 0ae0fa8b94..50dd4b4fc1 100644
--- a/src/calibre/gui2/dialogs/choose_format.ui
+++ b/src/calibre/gui2/dialogs/choose_format.ui
@@ -39,7 +39,7 @@
Qt::Horizontal
- QDialogButtonBox::Ok
+ QDialogButtonBox::Ok|QDialogButtonBox::Cancel
diff --git a/src/calibre/gui2/dialogs/config/add_save.py b/src/calibre/gui2/dialogs/config/add_save.py
index aff995d84f..b1f5621f44 100644
--- a/src/calibre/gui2/dialogs/config/add_save.py
+++ b/src/calibre/gui2/dialogs/config/add_save.py
@@ -45,6 +45,7 @@ class AddSave(QTabWidget, Ui_TabWidget):
self.metadata_box.layout().insertWidget(0, self.filename_pattern)
self.opt_swap_author_names.setChecked(prefs['swap_author_names'])
self.opt_add_formats_to_existing.setChecked(prefs['add_formats_to_existing'])
+ self.preserve_user_collections.setChecked(prefs['preserve_user_collections'])
help = '\n'.join(textwrap.wrap(c.get_option('template').help, 75))
self.save_template.initialize('save_to_disk', opts.template, help)
self.send_template.initialize('send_to_device', opts.send_template, help)
@@ -71,6 +72,7 @@ class AddSave(QTabWidget, Ui_TabWidget):
prefs['filename_pattern'] = pattern
prefs['swap_author_names'] = bool(self.opt_swap_author_names.isChecked())
prefs['add_formats_to_existing'] = bool(self.opt_add_formats_to_existing.isChecked())
+ prefs['preserve_user_collections'] = bool(self.preserve_user_collections.isChecked())
return True
diff --git a/src/calibre/gui2/dialogs/config/add_save.ui b/src/calibre/gui2/dialogs/config/add_save.ui
index 7fda2dbc7f..64a8137aa1 100644
--- a/src/calibre/gui2/dialogs/config/add_save.ui
+++ b/src/calibre/gui2/dialogs/config/add_save.ui
@@ -51,7 +51,7 @@
-
- If an existing book with a similar title and author is found that does not have the format being added, the format is added
+ If an existing book with a similar title and author is found that does not have the format being added, the format is added
to the existing book, instead of creating a new entry. If the existing book already has the format, then it is silently ignored.
Title match ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. Author match is exact.
@@ -179,7 +179,31 @@ Title match ignores leading indefinite articles ("the", "a",
-
-
+
+
+ Preserve device collections.
+
+
+
+ -
+
+
+ If checked, collections will not be deleted even if a book with changed metadata is resent and the collection is not in the book's metadata. In addition, editing collections in the device view will be enabled. If unchecked, collections will be always reflect only the metadata in the calibre library.
+
+
+ true
+
+
+
+ -
+
+
+
+
+
+
+ -
+
Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Plugins
diff --git a/src/calibre/gui2/dialogs/config/create_custom_column.py b/src/calibre/gui2/dialogs/config/create_custom_column.py
index a66b7b6642..2aae567b1c 100644
--- a/src/calibre/gui2/dialogs/config/create_custom_column.py
+++ b/src/calibre/gui2/dialogs/config/create_custom_column.py
@@ -24,16 +24,19 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
2:{'datatype':'comments',
'text':_('Long text, like comments, not shown in the tag browser'),
'is_multiple':False},
- 3:{'datatype':'datetime',
+ 3:{'datatype':'series',
+ 'text':_('Text column for keeping series-like information'),
+ 'is_multiple':False},
+ 4:{'datatype':'datetime',
'text':_('Date'), 'is_multiple':False},
- 4:{'datatype':'float',
+ 5:{'datatype':'float',
'text':_('Floating point numbers'), 'is_multiple':False},
- 5:{'datatype':'int',
+ 6:{'datatype':'int',
'text':_('Integers'), 'is_multiple':False},
- 6:{'datatype':'rating',
+ 7:{'datatype':'rating',
'text':_('Ratings, shown with stars'),
'is_multiple':False},
- 7:{'datatype':'bool',
+ 8:{'datatype':'bool',
'text':_('Yes/No'), 'is_multiple':False},
}
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py
index 8b27ff1999..9fcfe13253 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.py
+++ b/src/calibre/gui2/dialogs/metadata_bulk.py
@@ -10,7 +10,7 @@ from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.ebooks.metadata import string_to_authors, \
authors_to_string
-from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page
+from calibre.gui2.custom_column_widgets import populate_metadata_page
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
@@ -44,15 +44,14 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.central_widget.tabBar().setVisible(False)
else:
self.create_custom_column_editors()
-
self.exec_()
def create_custom_column_editors(self):
w = self.central_widget.widget(1)
layout = QGridLayout()
-
- self.custom_column_widgets, self.__cc_spacers = populate_bulk_metadata_page(
- layout, self.db, self.ids, w)
+ self.custom_column_widgets, self.__cc_spacers = \
+ populate_metadata_page(layout, self.db, self.ids, parent=w,
+ two_column=False, bulk=True)
w.setLayout(layout)
self.__custom_col_layouts = [layout]
ans = self.custom_column_widgets
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index 96323ac596..84b601776e 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -32,7 +32,7 @@ from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt
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_single_metadata_page
+from calibre.gui2.custom_column_widgets import populate_metadata_page
class CoverFetcher(QThread):
@@ -420,23 +420,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def create_custom_column_editors(self):
w = self.central_widget.widget(1)
- top_layout = QHBoxLayout()
- top_layout.setSpacing(20)
- left_layout = QGridLayout()
- right_layout = QGridLayout()
- top_layout.addLayout(left_layout)
-
- self.custom_column_widgets, self.__cc_spacers = populate_single_metadata_page(
- left_layout, right_layout, self.db, self.id, w)
- top_layout.addLayout(right_layout)
- sip.delete(w.layout())
- w.setLayout(top_layout)
- self.__custom_col_layouts = [top_layout, left_layout, right_layout]
+ layout = w.layout()
+ self.custom_column_widgets, self.__cc_spacers = \
+ populate_metadata_page(layout, self.db, self.id,
+ parent=w, bulk=False, two_column=True)
+ self.__custom_col_layouts = [layout]
ans = self.custom_column_widgets
for i in range(len(ans)-1):
- w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[-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 validate_isbn(self, isbn):
isbn = unicode(isbn).strip()
diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py
index 1ec80f4b4a..9eb368e5e4 100644
--- a/src/calibre/gui2/dialogs/tag_list_editor.py
+++ b/src/calibre/gui2/dialogs/tag_list_editor.py
@@ -1,54 +1,64 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal '
-from functools import partial
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QListWidgetItem
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
from calibre.gui2 import question_dialog, error_dialog
-from calibre.ebooks.metadata import title_sort
+
+class ListWidgetItem(QListWidgetItem):
+
+ def __init__(self, txt):
+ QListWidgetItem.__init__(self, txt)
+ self.old_value = txt
+ self.cur_value = txt
+
+ def data(self, role):
+ if role == Qt.DisplayRole:
+ if self.old_value != self.cur_value:
+ return _('%s (was %s)'%(self.cur_value, self.old_value))
+ else:
+ return self.cur_value
+ elif role == Qt.EditRole:
+ return self.cur_value
+ else:
+ return QListWidgetItem.data(self, role)
+
+ def setData(self, role, data):
+ if role == Qt.EditRole:
+ self.cur_value = data.toString()
+ QListWidgetItem.setData(self, role, data)
+
+ def text(self):
+ return self.cur_value
+
+ def setText(self, txt):
+ self.cur_value = txt
+ QListWidgetItem.setText(txt)
class TagListEditor(QDialog, Ui_TagListEditor):
- def __init__(self, window, db, tag_to_match, category):
+ def __init__(self, window, tag_to_match, data, compare):
QDialog.__init__(self, window)
Ui_TagListEditor.__init__(self)
self.setupUi(self)
self.to_rename = {}
self.to_delete = []
- self.db = db
self.all_tags = {}
- self.category = category
- if category == 'tags':
- result = db.get_tags_with_ids()
- compare = (lambda x,y:cmp(x.lower(), y.lower()))
- elif category == 'series':
- result = db.get_series_with_ids()
- compare = (lambda x,y:cmp(title_sort(x).lower(), title_sort(y).lower()))
- elif category == 'publisher':
- result = db.get_publishers_with_ids()
- compare = (lambda x,y:cmp(x.lower(), y.lower()))
- else: # should be a custom field
- self.cc_label = None
- if category in db.field_metadata:
- self.cc_label = db.field_metadata[category]['label']
- result = self.db.get_custom_items_with_ids(label=self.cc_label)
- else:
- result = []
- compare = (lambda x,y:cmp(x.lower(), y.lower()))
- for k,v in result:
+ for k,v in data:
self.all_tags[v] = k
for tag in sorted(self.all_tags.keys(), cmp=compare):
- item = QListWidgetItem(tag)
+ item = ListWidgetItem(tag)
item.setData(Qt.UserRole, self.all_tags[tag])
self.available_tags.addItem(item)
- items = self.available_tags.findItems(tag_to_match, Qt.MatchExactly)
- if len(items) == 1:
- self.available_tags.setCurrentItem(items[0])
+ if tag_to_match is not None:
+ items = self.available_tags.findItems(tag_to_match, Qt.MatchExactly)
+ if len(items) == 1:
+ self.available_tags.setCurrentItem(items[0])
self.connect(self.delete_button, SIGNAL('clicked()'), self.delete_tags)
self.connect(self.rename_button, SIGNAL('clicked()'), self.rename_tag)
@@ -62,13 +72,11 @@ class TagListEditor(QDialog, Ui_TagListEditor):
item.setText(self.item_before_editing.text())
return
if item.text() != self.item_before_editing.text():
- if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
- error_dialog(self, _('Item already used'),
- _('The item %s is already used.')%(item.text())).exec_()
- item.setText(self.item_before_editing.text())
- return
(id,ign) = self.item_before_editing.data(Qt.UserRole).toInt()
- self.to_rename[item.text()] = id
+ if item.text() not in self.to_rename:
+ self.to_rename[item.text()] = [id]
+ else:
+ self.to_rename[item.text()].append(id)
def rename_tag(self):
item = self.available_tags.currentItem()
@@ -99,30 +107,3 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.to_delete.append(id)
self.available_tags.takeItem(self.available_tags.row(item))
- def accept(self):
- rename_func = None
- if self.category == 'tags':
- rename_func = self.db.rename_tag
- delete_func = self.db.delete_tag_using_id
- elif self.category == 'series':
- rename_func = self.db.rename_series
- delete_func = self.db.delete_series_using_id
- elif self.category == 'publisher':
- rename_func = self.db.rename_publisher
- delete_func = self.db.delete_publisher_using_id
- else:
- rename_func = partial(self.db.rename_custom_item, label=self.cc_label)
- delete_func = partial(self.db.delete_custom_item_using_id, label=self.cc_label)
-
- work_done = False
- if rename_func:
- for text in self.to_rename:
- work_done = True
- rename_func(id=self.to_rename[text], new_name=unicode(text))
- for item in self.to_delete:
- work_done = True
- delete_func(item)
- if not work_done:
- QDialog.reject(self)
- else:
- QDialog.accept(self)
diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py
index b334808d9b..efbe32a04e 100644
--- a/src/calibre/gui2/init.py
+++ b/src/calibre/gui2/init.py
@@ -226,17 +226,30 @@ class LibraryViewMixin(object): # {{{
self.action_show_book_details,
self.action_del,
add_to_library = None,
+ edit_device_collections=None,
similar_menu=similar_menu)
add_to_library = (_('Add books to library'), self.add_books_from_device)
+
+ 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,
- add_to_library=add_to_library)
+ 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,
- add_to_library=add_to_library)
+ 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,
- add_to_library=add_to_library)
+ add_to_library=add_to_library,
+ edit_device_collections=edit_device_collections)
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
for func, args in [
@@ -249,9 +262,14 @@ class LibraryViewMixin(object): # {{{
getattr(view, func)(*args)
self.memory_view.connect_dirtied_signal(self.upload_booklists)
+ self.memory_view.connect_upload_collections_signal(
+ func=self.upload_collections, oncard=None)
self.card_a_view.connect_dirtied_signal(self.upload_booklists)
+ self.card_a_view.connect_upload_collections_signal(
+ func=self.upload_collections, oncard='carda')
self.card_b_view.connect_dirtied_signal(self.upload_booklists)
-
+ self.card_b_view.connect_upload_collections_signal(
+ func=self.upload_collections, oncard='cardb')
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index dcc338dbdc..fcbcf043fc 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -16,11 +16,12 @@ from calibre.gui2 import NONE, config, UNDEFINED_QDATE
from calibre.utils.pyparsing import ParseException
from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors
from calibre.ptempfile import PersistentTemporaryFile
-from calibre.utils.config import tweaks
+from calibre.utils.config import tweaks, prefs
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
+from calibre.library.cli import parse_series_string
from calibre import strftime, isbytestring, prepare_string_for_xml
from calibre.constants import filesystem_encoding
from calibre.gui2.library import DEFAULT_SORT
@@ -520,7 +521,7 @@ class BooksModel(QAbstractTableModel): # {{{
return QVariant(', '.join(sorted(tags.split(','))))
return None
- def series(r, idx=-1, siix=-1):
+ def series_type(r, idx=-1, siix=-1):
series = self.db.data[r][idx]
if series:
idx = fmt_sidx(self.db.data[r][siix])
@@ -591,7 +592,7 @@ class BooksModel(QAbstractTableModel): # {{{
idx=self.db.field_metadata['publisher']['rec_index'], mult=False),
'tags' : functools.partial(tags,
idx=self.db.field_metadata['tags']['rec_index']),
- 'series' : functools.partial(series,
+ 'series' : functools.partial(series_type,
idx=self.db.field_metadata['series']['rec_index'],
siix=self.db.field_metadata['series_index']['rec_index']),
'ondevice' : functools.partial(text_type,
@@ -620,6 +621,9 @@ class BooksModel(QAbstractTableModel): # {{{
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes')
elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx)
+ elif datatype == 'series':
+ self.dc[col] = functools.partial(series_type, idx=idx,
+ siix=self.db.field_metadata.cc_series_index_column_for(col))
else:
print 'What type is this?', col, datatype
# build a index column to data converter map, to remove the string lookup in the data loop
@@ -681,6 +685,8 @@ class BooksModel(QAbstractTableModel): # {{{
def set_custom_column_data(self, row, colhead, value):
typ = self.custom_columns[colhead]['datatype']
+ label=self.db.field_metadata.key_to_label(colhead)
+ s_index = None
if typ in ('text', 'comments'):
val = unicode(value.toString()).strip()
val = val if val else None
@@ -702,9 +708,10 @@ class BooksModel(QAbstractTableModel): # {{{
if not val.isValid():
return False
val = qt_to_dt(val, as_utc=False)
- self.db.set_custom(self.db.id(row), val,
- label=self.db.field_metadata.key_to_label(colhead),
- num=None, append=False, notify=True)
+ elif typ == 'series':
+ val, s_index = parse_series_string(self.db, label, value.toString())
+ self.db.set_custom(self.db.id(row), val, extra=s_index,
+ label=label, num=None, append=False, notify=True)
return True
def setData(self, index, value, role):
@@ -850,6 +857,7 @@ class OnDeviceSearch(SearchQueryParser): # {{{
class DeviceBooksModel(BooksModel): # {{{
booklist_dirtied = pyqtSignal()
+ upload_collections = pyqtSignal(object)
def __init__(self, parent):
BooksModel.__init__(self, parent)
@@ -920,11 +928,12 @@ class DeviceBooksModel(BooksModel): # {{{
if index.isValid() and self.editable:
cname = self.column_map[index.column()]
if cname in ('title', 'authors') or \
- (cname == 'collections' and self.db.supports_collections()):
+ (cname == 'collections' and \
+ self.db.supports_collections() and \
+ prefs['preserve_user_collections']):
flags |= Qt.ItemIsEditable
return flags
-
def search(self, text, reset=True):
if not text or not text.strip():
self.map = list(range(len(self.db)))
@@ -970,8 +979,8 @@ class DeviceBooksModel(BooksModel): # {{{
x, y = int(self.db[x].size), int(self.db[y].size)
return cmp(x, y)
def tagscmp(x, y):
- x = ','.join(self.db[x].device_collections)
- y = ','.join(self.db[y].device_collections)
+ x = ','.join(sorted(getattr(self.db[x], 'device_collections', []))).lower()
+ y = ','.join(sorted(getattr(self.db[y], 'device_collections', []))).lower()
return cmp(x, y)
def libcmp(x, y):
x, y = self.db[x].in_library, self.db[y].in_library
@@ -1072,6 +1081,36 @@ class DeviceBooksModel(BooksModel): # {{{
res.append((r,b))
return res
+ def get_collections_with_ids(self):
+ collections = set()
+ for book in self.db:
+ if book.device_collections is not None:
+ collections.update(set(book.device_collections))
+ self.collections = []
+ result = []
+ for i,collection in enumerate(collections):
+ result.append((i, collection))
+ self.collections.append(collection)
+ return result
+
+ def rename_collection(self, old_id, new_name):
+ old_name = self.collections[old_id]
+ for book in self.db:
+ if book.device_collections is None:
+ continue
+ if old_name in book.device_collections:
+ book.device_collections.remove(old_name)
+ if new_name not in book.device_collections:
+ book.device_collections.append(new_name)
+
+ def delete_collection_using_id(self, old_id):
+ old_name = self.collections[old_id]
+ for book in self.db:
+ if book.device_collections is None:
+ continue
+ if old_name in book.device_collections:
+ book.device_collections.remove(old_name)
+
def indices(self, rows):
'''
Return indices into underlying database from rows
@@ -1102,6 +1141,7 @@ class DeviceBooksModel(BooksModel): # {{{
elif cname == 'collections':
tags = self.db[self.map[row]].device_collections
if tags:
+ tags.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()))
return QVariant(', '.join(tags))
elif role == Qt.ToolTipRole and index.isValid():
if self.map[row] in self.indices_to_be_deleted():
@@ -1144,14 +1184,18 @@ class DeviceBooksModel(BooksModel): # {{{
return False
val = unicode(value.toString()).strip()
idx = self.map[row]
+ if cname == 'collections':
+ tags = [i.strip() for i in val.split(',')]
+ tags = [t for t in tags if t]
+ self.db[idx].device_collections = tags
+ self.dataChanged.emit(index, index)
+ self.upload_collections.emit(self.db)
+ return True
+
if cname == 'title' :
self.db[idx].title = val
elif cname == 'authors':
self.db[idx].authors = string_to_authors(val)
- elif cname == 'collections':
- tags = [i.strip() for i in val.split(',')]
- tags = [t for t in tags if t]
- self.db[idx].device_collections = tags
self.dataChanged.emit(index, index)
self.booklist_dirtied.emit()
done = True
diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py
index b949306294..09c1f8478b 100644
--- a/src/calibre/gui2/library/views.py
+++ b/src/calibre/gui2/library/views.py
@@ -15,7 +15,7 @@ from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
CcBoolDelegate, CcCommentsDelegate, CcDateDelegate
from calibre.gui2.library.models import BooksModel, DeviceBooksModel
-from calibre.utils.config import tweaks
+from calibre.utils.config import tweaks, prefs
from calibre.gui2 import error_dialog, gprefs
from calibre.gui2.library import DEFAULT_SORT
@@ -347,7 +347,7 @@ class BooksView(QTableView): # {{{
self.setItemDelegateForColumn(cm.index(colhead), delegate)
elif cc['datatype'] == 'comments':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
- elif cc['datatype'] == 'text':
+ elif cc['datatype'] in ('text', 'series'):
if cc['is_multiple']:
self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate)
else:
@@ -371,7 +371,8 @@ class BooksView(QTableView): # {{{
# Context Menu {{{
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
save, open_folder, book_details, delete,
- similar_menu=None, add_to_library=None):
+ similar_menu=None, add_to_library=None,
+ edit_device_collections=None):
self.setContextMenuPolicy(Qt.DefaultContextMenu)
self.context_menu = QMenu(self)
if edit_metadata is not None:
@@ -393,6 +394,10 @@ class BooksView(QTableView): # {{{
if add_to_library is not None:
func = partial(add_to_library[1], view=self)
self.context_menu.addAction(add_to_library[0], func)
+ if edit_device_collections is not None:
+ func = partial(edit_device_collections[1], view=self)
+ self.edit_collections_menu = \
+ self.context_menu.addAction(edit_device_collections[0], func)
def contextMenuEvent(self, event):
self.context_menu.popup(event.globalPos())
@@ -494,6 +499,13 @@ class DeviceBooksView(BooksView): # {{{
self.setDragDropMode(self.NoDragDrop)
self.setAcceptDrops(False)
+ def contextMenuEvent(self, event):
+ self.edit_collections_menu.setVisible(
+ self._model.db.supports_collections() and \
+ prefs['preserve_user_collections'])
+ self.context_menu.popup(event.globalPos())
+ event.accept()
+
def set_database(self, db):
self._model.set_database(db)
self.restore_state()
@@ -505,6 +517,9 @@ class DeviceBooksView(BooksView): # {{{
def connect_dirtied_signal(self, slot):
self._model.booklist_dirtied.connect(slot)
+ def connect_upload_collections_signal(self, func=None, oncard=None):
+ self._model.upload_collections.connect(partial(func, view=self, oncard=oncard))
+
def dropEvent(self, *args):
error_dialog(self, _('Not allowed'),
_('Dropping onto a device is not supported. First add the book to the calibre library.')).exec_()
diff --git a/src/calibre/gui2/pictureflow/pictureflow.cpp b/src/calibre/gui2/pictureflow/pictureflow.cpp
index d1434e763c..a100f60e75 100644
--- a/src/calibre/gui2/pictureflow/pictureflow.cpp
+++ b/src/calibre/gui2/pictureflow/pictureflow.cpp
@@ -75,10 +75,6 @@
#include
-// uncomment this to enable bilinear filtering for texture mapping
-// gives much better rendering, at the cost of memory space
-// #define PICTUREFLOW_BILINEAR_FILTER
-
// for fixed-point arithmetic, we need minimum 32-bit long
// long long (64-bit) might be useful for multiplication and division
typedef long PFreal;
@@ -376,7 +372,6 @@ private:
int slideWidth;
int slideHeight;
int fontSize;
- int zoom;
int queueLength;
int centerIndex;
@@ -401,6 +396,7 @@ private:
void recalc(int w, int h);
QRect renderSlide(const SlideInfo &slide, int alpha=256, int col1=-1, int col=-1);
+ QRect renderCenterSlide(const SlideInfo &slide);
QImage* surface(int slideIndex);
void triggerRender();
void resetSlides();
@@ -414,7 +410,6 @@ PictureFlowPrivate::PictureFlowPrivate(PictureFlow* w, int queueLength_)
slideWidth = 200;
slideHeight = 200;
fontSize = 10;
- zoom = 100;
centerIndex = 0;
queueLength = queueLength_;
@@ -464,21 +459,6 @@ void PictureFlowPrivate::setSlideSize(QSize size)
triggerRender();
}
-int PictureFlowPrivate::zoomFactor() const
-{
- return zoom;
-}
-
-void PictureFlowPrivate::setZoomFactor(int z)
-{
- if(z <= 0)
- return;
-
- zoom = z;
- recalc(buffer.width(), buffer.height());
- triggerRender();
-}
-
QImage PictureFlowPrivate::slide(int index) const
{
return slideImages->image(index);
@@ -554,7 +534,8 @@ void PictureFlowPrivate::resize(int w, int h)
if (w < 10) w = 10;
if (h < 10) h = 10;
slideHeight = int(float(h)/REFLECTION_FACTOR);
- slideWidth = int(float(slideHeight) * 2/3.);
+ slideWidth = int(float(slideHeight) * 3./4.);
+ //qDebug() << slideHeight << "x" << slideWidth;
fontSize = MAX(int(h/15.), 12);
recalc(w, h);
resetSlides();
@@ -595,15 +576,12 @@ void PictureFlowPrivate::resetSlides()
}
}
-#define BILINEAR_STRETCH_HOR 4
-#define BILINEAR_STRETCH_VER 4
-
static QImage prepareSurface(QImage img, int w, int h)
{
Qt::TransformationMode mode = Qt::SmoothTransformation;
- img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode);
+ img = img.scaled(w, h, Qt::KeepAspectRatioByExpanding, mode);
- // slightly larger, to accomodate for the reflection
+ // slightly larger, to accommodate for the reflection
int hs = int(h * REFLECTION_FACTOR);
int hofs = 0;
@@ -633,12 +611,6 @@ static QImage prepareSurface(QImage img, int w, int h)
result.setPixel(h+hofs+y, x, qRgb(r, g, b));
}
-#ifdef PICTUREFLOW_BILINEAR_FILTER
- int hh = BILINEAR_STRETCH_VER*hs;
- int ww = BILINEAR_STRETCH_HOR*w;
- result = result.scaled(hh, ww, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
-#endif
-
return result;
}
@@ -699,8 +671,12 @@ void PictureFlowPrivate::render()
int nleft = leftSlides.count();
int nright = rightSlides.count();
+ QRect r;
- QRect r = renderSlide(centerSlide);
+ if (step == 0)
+ r = renderCenterSlide(centerSlide);
+ else
+ r = renderSlide(centerSlide);
int c1 = r.left();
int c2 = r.right();
@@ -813,7 +789,23 @@ static inline uint BYTE_MUL_RGB16_32(uint x, uint a) {
return t;
}
+QRect PictureFlowPrivate::renderCenterSlide(const SlideInfo &slide) {
+ QImage* src = surface(slide.slideIndex);
+ if(!src)
+ return QRect();
+ int sw = src->height();
+ int sh = src->width();
+ int h = buffer.height();
+ QRect rect(buffer.width()/2 - sw/2, 0, sw, h-1);
+ int left = rect.left();
+
+ for(int x = 0; x < sh-1; x++)
+ for(int y = 0; y < sw; y++)
+ buffer.setPixel(left + y, 1+x, src->pixel(x, y));
+
+ return rect;
+}
// Renders a slide to offscreen buffer. Returns a rect of the rendered area.
// alpha=256 means normal, alpha=0 is fully black, alpha=128 half transparent
// col1 and col2 limit the column for rendering.
@@ -826,13 +818,8 @@ int col1, int col2)
QRect rect(0, 0, 0, 0);
-#ifdef PICTUREFLOW_BILINEAR_FILTER
- int sw = src->height() / BILINEAR_STRETCH_HOR;
- int sh = src->width() / BILINEAR_STRETCH_VER;
-#else
int sw = src->height();
int sh = src->width();
-#endif
int h = buffer.height();
int w = buffer.width();
@@ -848,7 +835,7 @@ int col1, int col2)
col1 = qMin(col1, w-1);
col2 = qMin(col2, w-1);
- int distance = h * 100 / zoom;
+ int distance = h;
PFreal sdx = fcos(slide.angle);
PFreal sdy = fsin(slide.angle);
PFreal xs = slide.cx - slideWidth * sdx/2;
@@ -878,15 +865,9 @@ int col1, int col2)
PFreal hitx = fmul(dist, rays[x]);
PFreal hitdist = fdiv(hitx - slide.cx, sdx);
-#ifdef PICTUREFLOW_BILINEAR_FILTER
- int column = sw*BILINEAR_STRETCH_HOR/2 + (hitdist*BILINEAR_STRETCH_HOR >> PFREAL_SHIFT);
- if(column >= sw*BILINEAR_STRETCH_HOR)
- break;
-#else
int column = sw/2 + (hitdist >> PFREAL_SHIFT);
if(column >= sw)
break;
-#endif
if(column < 0)
continue;
@@ -901,13 +882,8 @@ int col1, int col2)
QRgb565* pixel2 = (QRgb565*)(buffer.scanLine(y2)) + x;
int pixelstep = pixel2 - pixel1;
-#ifdef PICTUREFLOW_BILINEAR_FILTER
- int center = (sh*BILINEAR_STRETCH_VER/2);
- int dy = dist*BILINEAR_STRETCH_VER / h;
-#else
int center = sh/2;
int dy = dist / h;
-#endif
int p1 = center*PFREAL_ONE - dy/2;
int p2 = center*PFREAL_ONE + dy/2;
@@ -1155,16 +1131,6 @@ void PictureFlow::setSlideSize(QSize size)
d->setSlideSize(size);
}
-int PictureFlow::zoomFactor() const
-{
- return d->zoomFactor();
-}
-
-void PictureFlow::setZoomFactor(int z)
-{
- d->setZoomFactor(z);
-}
-
QImage PictureFlow::slide(int index) const
{
return d->slide(index);
diff --git a/src/calibre/gui2/pictureflow/pictureflow.h b/src/calibre/gui2/pictureflow/pictureflow.h
index 8cce025180..13477a8771 100644
--- a/src/calibre/gui2/pictureflow/pictureflow.h
+++ b/src/calibre/gui2/pictureflow/pictureflow.h
@@ -91,7 +91,6 @@ Q_OBJECT
Q_PROPERTY(int currentSlide READ currentSlide WRITE setCurrentSlide)
Q_PROPERTY(QSize slideSize READ slideSize WRITE setSlideSize)
- Q_PROPERTY(int zoomFactor READ zoomFactor WRITE setZoomFactor)
public:
/*!
@@ -120,16 +119,6 @@ public:
*/
void setSlideSize(QSize size);
- /*!
- Sets the zoom factor (in percent).
- */
- void setZoomFactor(int zoom);
-
- /*!
- Returns the zoom factor (in percent).
- */
- int zoomFactor() const;
-
/*!
Clears any caches held to free up memory
*/
diff --git a/src/calibre/gui2/pictureflow/pictureflow.sip b/src/calibre/gui2/pictureflow/pictureflow.sip
index 9202dd8ad5..f7ba12cee7 100644
--- a/src/calibre/gui2/pictureflow/pictureflow.sip
+++ b/src/calibre/gui2/pictureflow/pictureflow.sip
@@ -40,10 +40,6 @@ public :
void setSlideSize(QSize size);
- void setZoomFactor(int zoom);
-
- int zoomFactor() const;
-
void clearCaches();
virtual QImage slide(int index) const;
diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py
index 35bf7374a0..ef0c6b1455 100644
--- a/src/calibre/gui2/search_box.py
+++ b/src/calibre/gui2/search_box.py
@@ -56,7 +56,8 @@ class SearchBox2(QComboBox):
To use this class:
* Call initialize()
- * Connect to the search() and cleared() signals from this widget
+ * Connect to the search() and cleared() signals from this widget.
+ * Connect to the cleared() signal to know when the box content changes
* Call search_done() after every search is complete
* Use clear() to clear back to the help message
'''
@@ -75,6 +76,7 @@ class SearchBox2(QComboBox):
type=Qt.DirectConnection)
self.line_edit.mouse_released.connect(self.mouse_released,
type=Qt.DirectConnection)
+ self.activated.connect(self.history_selected)
self.setEditable(True)
self.help_state = False
self.as_you_type = True
@@ -139,6 +141,9 @@ class SearchBox2(QComboBox):
def key_pressed(self, event):
self.normalize_state()
+ if self._in_a_search:
+ self.emit(SIGNAL('changed()'))
+ 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)
@@ -154,6 +159,10 @@ class SearchBox2(QComboBox):
self.timer = None
self.do_search()
+ def history_selected(self, text):
+ self.emit(SIGNAL('changed()'))
+ self.do_search()
+
@property
def smart_text(self):
text = unicode(self.currentText()).strip()
@@ -345,6 +354,7 @@ class SearchBoxMixin(object):
self.search.initialize('main_search_history', colorize=True,
help_text=_('Search (For Advanced Search click the button to the left)'))
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
+ self.connect(self.search, SIGNAL('changed()'), self.search_box_changed)
self.connect(self.clear_button, SIGNAL('clicked()'), self.search.clear)
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
self.do_advanced_search)
@@ -364,6 +374,9 @@ class SearchBoxMixin(object):
self.saved_search.clear_to_help()
self.set_number_of_books_shown()
+ def search_box_changed(self):
+ self.tags_view.clear()
+
def do_advanced_search(self, *args):
d = SearchDialog(self)
if d.exec_() == QDialog.Accepted:
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index daea4e86ea..189caea6ea 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -15,6 +15,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
QPushButton, QWidget, QItemDelegate
+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
@@ -680,9 +681,50 @@ class TagBrowserMixin(object): # {{{
self.tags_view.recount()
def do_tags_list_edit(self, tag, category):
- d = TagListEditor(self, self.library_view.model().db, tag, category)
+ db=self.library_view.model().db
+ if category == 'tags':
+ result = db.get_tags_with_ids()
+ compare = (lambda x,y:cmp(x.lower(), y.lower()))
+ elif category == 'series':
+ result = db.get_series_with_ids()
+ compare = (lambda x,y:cmp(title_sort(x).lower(), title_sort(y).lower()))
+ elif category == 'publisher':
+ result = db.get_publishers_with_ids()
+ compare = (lambda x,y:cmp(x.lower(), y.lower()))
+ else: # should be a custom field
+ cc_label = None
+ if category in db.field_metadata:
+ cc_label = db.field_metadata[category]['label']
+ result = self.db.get_custom_items_with_ids(label=cc_label)
+ else:
+ result = []
+ compare = (lambda x,y:cmp(x.lower(), y.lower()))
+
+ d = TagListEditor(self, tag_to_match=tag, data=result, compare=compare)
d.exec_()
if d.result() == d.Accepted:
+ to_rename = d.to_rename # dict of new text to old id
+ to_delete = d.to_delete # list of ids
+ rename_func = None
+ if category == 'tags':
+ rename_func = db.rename_tag
+ delete_func = db.delete_tag_using_id
+ elif category == 'series':
+ rename_func = db.rename_series
+ delete_func = db.delete_series_using_id
+ elif category == 'publisher':
+ rename_func = db.rename_publisher
+ delete_func = db.delete_publisher_using_id
+ else:
+ rename_func = partial(db.rename_custom_item, label=cc_label)
+ delete_func = partial(db.delete_custom_item_using_id, label=cc_label)
+ if rename_func:
+ for text in to_rename:
+ for old_id in to_rename[text]:
+ rename_func(old_id, new_name=unicode(text))
+ for item in to_delete:
+ delete_func(item)
+
# Clean up everything, as information could have changed for many books.
self.library_view.model().refresh()
self.tags_view.set_new_model()
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index 6452890883..590329ec13 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -473,6 +473,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.search_restriction.setEnabled(False)
for action in list(self.delete_menu.actions())[1:]:
action.setEnabled(False)
+ # Reset the view in case something changed while it was invisible
+ self.current_view().reset()
self.set_number_of_books_shown()
diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py
index c32b79fb7c..33ee25c433 100644
--- a/src/calibre/gui2/widgets.py
+++ b/src/calibre/gui2/widgets.py
@@ -957,16 +957,19 @@ class LayoutButton(QToolButton):
self.splitter = splitter
splitter.state_changed.connect(self.update_state)
+ self.setCursor(Qt.PointingHandCursor)
def set_state_to_show(self, *args):
self.setChecked(False)
label =_('Show')
self.setText(label + ' ' + self.label)
+ self.setToolTip(self.text())
def set_state_to_hide(self, *args):
self.setChecked(True)
label = _('Hide')
self.setText(label + ' ' + self.label)
+ self.setToolTip(self.text())
def update_state(self, *args):
if self.splitter.is_side_index_hidden:
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index 57f9d0baaf..06cf07bb67 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -401,7 +401,8 @@ class ResultCache(SearchQueryParser):
for x in self.field_metadata:
if len(self.field_metadata[x]['search_terms']):
db_col[x] = self.field_metadata[x]['rec_index']
- if self.field_metadata[x]['datatype'] not in ['text', 'comments']:
+ if self.field_metadata[x]['datatype'] not in \
+ ['text', 'comments', 'series']:
exclude_fields.append(db_col[x])
col_datatype[db_col[x]] = self.field_metadata[x]['datatype']
is_multiple_cols[db_col[x]] = self.field_metadata[x]['is_multiple']
@@ -580,16 +581,18 @@ class ResultCache(SearchQueryParser):
self.sort(field, ascending)
self._map_filtered = list(self._map)
- def seriescmp(self, x, y):
- sidx = self.FIELD_MAP['series']
+ def seriescmp(self, sidx, siidx, x, y, library_order=None):
try:
- ans = cmp(title_sort(self._data[x][sidx].lower()),
- title_sort(self._data[y][sidx].lower()))
+ if library_order:
+ ans = cmp(title_sort(self._data[x][sidx].lower()),
+ title_sort(self._data[y][sidx].lower()))
+ else:
+ ans = cmp(self._data[x][sidx].lower(),
+ self._data[y][sidx].lower())
except AttributeError: # Some entries may be None
ans = cmp(self._data[x][sidx], self._data[y][sidx])
if ans != 0: return ans
- sidx = self.FIELD_MAP['series_index']
- return cmp(self._data[x][sidx], self._data[y][sidx])
+ return cmp(self._data[x][siidx], self._data[y][siidx])
def cmp(self, loc, x, y, asstr=True, subsort=False):
try:
@@ -617,18 +620,27 @@ class ResultCache(SearchQueryParser):
elif field == 'title': field = 'sort'
elif field == 'authors': field = 'author_sort'
as_string = field not in ('size', 'rating', 'timestamp')
- if self.field_metadata[field]['is_custom']:
- as_string = self.field_metadata[field]['datatype'] in ('comments', 'text')
- field = self.field_metadata[field]['colnum']
if self.first_sort:
subsort = True
self.first_sort = False
- fcmp = self.seriescmp \
- if field == 'series' and \
- tweaks['title_series_sorting'] == 'library_order' \
- else \
- functools.partial(self.cmp, self.FIELD_MAP[field],
+ if self.field_metadata[field]['is_custom']:
+ if self.field_metadata[field]['datatype'] == 'series':
+ fcmp = functools.partial(self.seriescmp,
+ self.field_metadata[field]['rec_index'],
+ self.field_metadata.cc_series_index_column_for(field),
+ library_order=tweaks['title_series_sorting'] == 'library_order')
+ else:
+ as_string = self.field_metadata[field]['datatype'] in ('comments', 'text')
+ field = self.field_metadata[field]['colnum']
+ fcmp = functools.partial(self.cmp, self.FIELD_MAP[field],
+ subsort=subsort, asstr=as_string)
+ elif field == 'series':
+ fcmp = functools.partial(self.seriescmp, self.FIELD_MAP['series'],
+ self.FIELD_MAP['series_index'],
+ library_order=tweaks['title_series_sorting'] == 'library_order')
+ else:
+ fcmp = functools.partial(self.cmp, self.FIELD_MAP[field],
subsort=subsort, asstr=as_string)
self._map.sort(cmp=fcmp, reverse=not ascending)
self._map_filtered = [id for id in self._map if id in self._map_filtered]
diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py
index 3f71c98238..058b879b55 100644
--- a/src/calibre/library/cli.py
+++ b/src/calibre/library/cli.py
@@ -7,11 +7,11 @@ __docformat__ = 'restructuredtext en'
Command line interface to the calibre database.
'''
-import sys, os, cStringIO
+import sys, os, cStringIO, re
from textwrap import TextWrapper
from calibre import terminal_controller, preferred_encoding, prints
-from calibre.utils.config import OptionParser, prefs
+from calibre.utils.config import OptionParser, prefs, tweaks
from calibre.ebooks.metadata.meta import get_metadata
from calibre.library.database2 import LibraryDatabase2
from calibre.ebooks.metadata.opf2 import OPFCreator, OPF
@@ -680,9 +680,31 @@ def command_catalog(args, dbpath):
# end of GR additions
+def parse_series_string(db, label, value):
+ val = unicode(value).strip()
+ s_index = None
+ pat = re.compile(r'\[([.0-9]+)\]')
+ match = pat.search(val)
+ if match is not None:
+ val = pat.sub('', val).strip()
+ s_index = float(match.group(1))
+ elif val:
+ if tweaks['series_index_auto_increment'] == 'next':
+ s_index = db.get_next_cc_series_num_for(val, label=label)
+ else:
+ s_index = 1.0
+ return val, s_index
+
def do_set_custom(db, col, id_, val, append):
- db.set_custom(id_, val, label=col, append=append)
- prints('Data set to: %r'%db.get_custom(id_, label=col, index_is_id=True))
+ if db.custom_column_label_map[col]['datatype'] == 'series':
+ val, s_index = parse_series_string(db, col, val)
+ db.set_custom(id_, val, extra=s_index, label=col, append=append)
+ prints('Data set to: %r[%4.2f]'%
+ (db.get_custom(id_, label=col, index_is_id=True),
+ db.get_custom_extra(id_, label=col, index_is_id=True)))
+ else:
+ db.set_custom(id_, val, label=col, append=append)
+ prints('Data set to: %r'%db.get_custom(id_, label=col, index_is_id=True))
def set_custom_option_parser():
parser = get_parser(_(
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index cc690c54db..e039f5a817 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
import json
from functools import partial
+from math import floor
from calibre import prints
from calibre.constants import preferred_encoding
@@ -16,7 +17,7 @@ from calibre.utils.date import parse_date
class CustomColumns(object):
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
- 'int', 'float', 'bool'])
+ 'int', 'float', 'bool', 'series'])
def custom_table_names(self, num):
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
@@ -137,7 +138,8 @@ class CustomColumns(object):
'bool': adapt_bool,
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
'datetime' : adapt_datetime,
- 'text':adapt_text
+ 'text':adapt_text,
+ 'series':adapt_text
}
# Create Tag Browser categories for custom columns
@@ -171,6 +173,19 @@ class CustomColumns(object):
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
return ans
+ def get_custom_extra(self, idx, label=None, num=None, index_is_id=False):
+ if label is not None:
+ data = self.custom_column_label_map[label]
+ if num is not None:
+ data = self.custom_column_num_map[num]
+ # add future datatypes with an extra column here
+ if data['datatype'] not in ['series']:
+ return None
+ ign,lt = self.custom_table_names(data['num'])
+ idx = idx if index_is_id else self.id(idx)
+ return self.conn.get('''SELECT extra FROM %s
+ WHERE book=?'''%lt, (idx,), all=False)
+
# convenience methods for tag editing
def get_custom_items_with_ids(self, label=None, num=None):
if label is not None:
@@ -220,6 +235,28 @@ class CustomColumns(object):
self.conn.commit()
# end convenience methods
+ def get_next_cc_series_num_for(self, series, label=None, num=None):
+ if label is not None:
+ data = self.custom_column_label_map[label]
+ if num is not None:
+ data = self.custom_column_num_map[num]
+ if data['datatype'] != 'series':
+ return None
+ table, lt = self.custom_table_names(data['num'])
+ # get the id of the row containing the series string
+ series_id = self.conn.get('SELECT id from %s WHERE value=?'%table,
+ (series,), all=False)
+ if series_id is None:
+ return 1.0
+ # get the label of the associated series number table
+ series_num = self.conn.get('''
+ SELECT MAX({lt}.extra) FROM {lt}
+ WHERE {lt}.book IN (SELECT book FROM {lt} where value=?)
+ '''.format(lt=lt), (series_id,), all=False)
+ if series_num is None:
+ return 1.0
+ return floor(series_num+1)
+
def all_custom(self, label=None, num=None):
if label is not None:
data = self.custom_column_label_map[label]
@@ -271,9 +308,8 @@ class CustomColumns(object):
self.conn.commit()
return changed
-
-
- def set_custom(self, id_, val, label=None, num=None, append=False, notify=True):
+ def set_custom(self, id_, val, label=None, num=None,
+ append=False, notify=True, extra=None):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
@@ -317,10 +353,17 @@ class CustomColumns(object):
'INSERT INTO %s(value) VALUES(?)'%table, (x,)).lastrowid
if not self.conn.get(
'SELECT book FROM %s WHERE book=? AND value=?'%lt,
- (id_, xid), all=False):
- self.conn.execute(
- 'INSERT INTO %s(book, value) VALUES (?,?)'%lt,
- (id_, xid))
+ (id_, xid), all=False):
+ if data['datatype'] == 'series':
+ self.conn.execute(
+ '''INSERT INTO %s(book, value, extra)
+ VALUES (?,?,?)'''%lt, (id_, xid, extra))
+ self.data.set(id_, self.FIELD_MAP[data['num']]+1,
+ extra, row_is_id=True)
+ else:
+ self.conn.execute(
+ '''INSERT INTO %s(book, value)
+ VALUES (?,?)'''%lt, (id_, xid))
self.conn.commit()
nval = self.conn.get(
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
@@ -370,6 +413,9 @@ class CustomColumns(object):
{table} ON(link.value={table}.id) WHERE link.book=books.id)
custom_{num}
'''.format(query=query%table, lt=lt, table=table, num=data['num'])
+ if data['datatype'] == 'series':
+ line += ''',(SELECT extra FROM {lt} WHERE {lt}.book=books.id)
+ custom_index_{num}'''.format(lt=lt, num=data['num'])
else:
line = '''
(SELECT value FROM {table} WHERE book=books.id) custom_{num}
@@ -393,7 +439,7 @@ class CustomColumns(object):
if datatype in ('rating', 'int'):
dt = 'INT'
- elif datatype in ('text', 'comments'):
+ elif datatype in ('text', 'comments', 'series'):
dt = 'TEXT'
elif datatype in ('float',):
dt = 'REAL'
@@ -404,6 +450,10 @@ class CustomColumns(object):
collate = 'COLLATE NOCASE' if dt == 'TEXT' else ''
table, lt = self.custom_table_names(num)
if normalized:
+ if datatype == 'series':
+ s_index = 'extra REAL,'
+ else:
+ s_index = ''
lines = [
'''\
CREATE TABLE %s(
@@ -419,8 +469,9 @@ class CustomColumns(object):
id INTEGER PRIMARY KEY AUTOINCREMENT,
book INTEGER NOT NULL,
value INTEGER NOT NULL,
+ %s
UNIQUE(book, value)
- );'''%lt,
+ );'''%(lt, s_index),
'CREATE INDEX %s_aidx ON %s (value);'%(lt,lt),
'CREATE INDEX %s_bidx ON %s (book);'%(lt,lt),
@@ -468,7 +519,7 @@ class CustomColumns(object):
ratings as r
WHERE {lt}.value={table}.id and bl.book={lt}.book and
r.id = bl.rating and r.rating <> 0) avg_rating,
- value AS sort
+ value AS sort
FROM {table};
CREATE VIEW tag_browser_filtered_{table} AS SELECT
@@ -483,7 +534,7 @@ class CustomColumns(object):
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
r.id = bl.rating AND r.rating <> 0 AND
books_list_filter(bl.book)) avg_rating,
- value AS sort
+ value AS sort
FROM {table};
'''.format(lt=lt, table=table),
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index fe4aac12b5..2983ac5e58 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -237,6 +237,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.custom_column_num_map[col]['label'],
base,
prefer_custom=True)
+ if self.custom_column_num_map[col]['datatype'] == 'series':
+ # account for the series index column. Field_metadata knows that
+ # the series index is one larger than the series. If you change
+ # it here, be sure to change it there as well.
+ self.FIELD_MAP[str(col)+'_s_index'] = base = base+1
self.FIELD_MAP['cover'] = base+1
self.field_metadata.set_field_record_index('cover', base+1, prefer_custom=False)
@@ -777,6 +782,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
icon=icon, tooltip=tooltip)
for r in data if item_not_zero_func(r)]
+ # Needed for legacy databases that have multiple ratings that
+ # map to n stars
+ for r in categories['rating']:
+ for x in categories['rating']:
+ if r.name == x.name and r.id != x.id:
+ r.count = r.count + x.count
+ categories['rating'].remove(x)
+ break
+
# We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically
categories['formats'] = []
diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py
index 8cb5c9bdad..5ccc17d1eb 100644
--- a/src/calibre/library/field_metadata.py
+++ b/src/calibre/library/field_metadata.py
@@ -81,7 +81,7 @@ class FieldMetadata(dict):
'column':'name',
'link_column':'series',
'category_sort':'(title_sort(name))',
- 'datatype':'text',
+ 'datatype':'series',
'is_multiple':None,
'kind':'field',
'name':_('Series'),
@@ -398,6 +398,8 @@ class FieldMetadata(dict):
if val['is_category'] and val['kind'] in ('user', 'search'):
del self._tb_cats[key]
+ def cc_series_index_column_for(self, key):
+ return self._tb_cats[key]['rec_index'] + 1
def add_user_category(self, label, name):
if label in self._tb_cats:
diff --git a/src/calibre/manual/conf.py b/src/calibre/manual/conf.py
index b00a454237..3866008f1f 100644
--- a/src/calibre/manual/conf.py
+++ b/src/calibre/manual/conf.py
@@ -100,9 +100,8 @@ html_use_smartypants = True
html_title = 'calibre User Manual'
html_short_title = 'Start'
html_logo = 'resources/logo.png'
-epub_titlepage = 'resources/titlepage.html'
-epub_logo = 'resources/logo.png'
epub_author = 'Kovid Goyal'
+epub_cover = 'resources/epub_cover.jpg'
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
diff --git a/src/calibre/manual/custom.py b/src/calibre/manual/custom.py
index 38486cc67a..917b927086 100644
--- a/src/calibre/manual/custom.py
+++ b/src/calibre/manual/custom.py
@@ -304,9 +304,8 @@ def auto_member(dirname, arguments, options, content, lineno,
return list(node)
def setup(app):
- app.add_config_value('epub_titlepage', None, False)
+ app.add_config_value('epub_cover', None, False)
app.add_config_value('epub_author', '', False)
- app.add_config_value('epub_logo', None, False)
app.add_builder(CustomBuilder)
app.add_builder(CustomQtBuild)
app.add_builder(EPUBHelpBuilder)
diff --git a/src/calibre/manual/epub.py b/src/calibre/manual/epub.py
index 4635e334c0..d54eb99a8d 100644
--- a/src/calibre/manual/epub.py
+++ b/src/calibre/manual/epub.py
@@ -50,6 +50,7 @@ OPF = '''\
{uid}
{date}
+