Merge from trunk

This commit is contained in:
Charles Haley 2010-06-15 14:40:08 +01:00
commit 35141a6abb
32 changed files with 1844 additions and 968 deletions

View File

@ -71,3 +71,10 @@ gui_pubdate_display_format = 'MMM yyyy'
# order until the title is edited. Double-clicking on a title and hitting return
# without changing anything is sufficient to change the sort.
title_series_sorting = 'library_order'
# How to render average rating in the tag browser.
# There are two rendering methods available. The first is to show a partial
# star, and the second is to show a partially filled rectangle. The first is
# better looking, but uses more screen space than the second.
# Values are 'star' or 'rectangle'
render_avg_rating_using='star'

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'GabrieleMarini, based on Darko Miletic'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>, Gabriele Marini'
__version__ = 'v1.02 Marini Gabriele '
__date__ = '14062010'
__description__ = 'Italian daily newspaper'
'''
http://www.corrieredellosport.it/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Auto(BasicNewsRecipe):
__author__ = 'Gabriele Marini'
description = 'Auto and Formula 1'
cover_url = 'http://www.auto.it/res/imgs/logo_Auto.png'
title = u'Auto'
publisher = 'CONTE Editore'
category = 'Sport'
language = 'it'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 60
max_articles_per_feed = 30
use_embedded_content = False
recursion = 10
remove_javascript = True
no_stylesheets = True
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
keep_only_tags = [
dict(name='h2', attrs={'class':['tit_Article y_Txt']}),
dict(name='h2', attrs={'class':['tit_Article']}),
dict(name='div', attrs={'class':['box_Img newsdet_new ']}),
dict(name='div', attrs={'class':['box_Img newsdet_as ']}),
dict(name='table', attrs={'class':['table_A']}),
dict(name='div', attrs={'class':['txt_Article txtBox_cms']}),
dict(name='testoscheda')]
feeds = [
(u'Tutte le News' , u'http://www.auto.it/rss/articoli.xml' ),
(u'Prove su Strada' , u'http://www.auto.it/rss/prove+6.xml'),
(u'Novit\xe0' , u'http://www.auto.it/rss/novita+3.xml')
]

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'GabrieleMarini, based on Darko Miletic'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>, Gabriele Marini'
__version__ = ' '
__date__ = '14-06-2010'
__description__ = 'Italian daily newspaper'
'''
http://www.corrieredellosport.it/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class ilCorrieredelloSport(BasicNewsRecipe):
__author__ = 'Gabriele Marini'
description = 'Italian daily newspaper'
cover_url = 'http://edicola.corrieredellosport.it/newsmem/corsport/prima/nazionale_prima.jpg'
title = u'Il Corriere dello Sport'
publisher = 'CORRIERE DELLO SPORT s.r.l. '
category = 'Sport'
language = 'it'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 10
max_articles_per_feed = 100
use_embedded_content = False
recursion = 10
remove_javascript = True
no_stylesheets = True
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
keep_only_tags = [
dict(name='h1', attrs={'class':['tit_Article']}),
dict(name='h1', attrs={'class':['tit_Article_mondiali']}),
dict(name='div', attrs={'class':['box_Img']}),
dict(name='p', attrs={'class':['summary','text']})]
feeds = [
(u'Primo Piano' , u'http://www.corrieredellosport.it/rss/primo_piano.xml' ),
(u'Calcio' , u'http://www.corrieredellosport.it/rss/Calcio-3.xml'),
(u'Formula 1' , u'http://www.corrieredellosport.it/rss/Formula-1-7.xml'),
(u'Moto' , u'http://www.corrieredellosport.it/rss/Moto-8.xml'),
(u'Piu visti' , u'http://www.corrieredellosport.it/rss/piu_visti.xml')
]

View File

@ -7,12 +7,33 @@ __docformat__ = 'restructuredtext en'
'''
Device driver for Amazon's Kindle
'''
import datetime, os, re, sys
import datetime, os, re, sys, json, hashlib
from cStringIO import StringIO
from struct import unpack
from calibre.devices.usbms.driver import USBMS
'''
Notes on collections:
A collections cache is stored at system/collections.json
The cache is read only, changes made to it are overwritten (it is regenerated)
on device disconnect
A log of collection creation/manipulation is available at
system/userannotationlog
collections.json refers to books via a SHA1 hash of the absolute path to the
book (prefix is /mnt/us on my Kindle). The SHA1 hash may or may not be prefixed
by some characters, use the last 40 characters.
Changing the metadata and resending the file doesn't seem to affect collections
Adding a book to a collection on the Kindle does not change the book file at all
(i.e. it is binary identical). Therefore collection information is not stored in
file metadata.
'''
class KINDLE(USBMS):
name = 'Kindle Device Interface'
@ -60,6 +81,7 @@ class KINDLE(USBMS):
'replace')
return mi
def get_annotations(self, path_map):
MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt']
mbp_formats = set(MBP_FORMATS)
@ -150,6 +172,37 @@ class KINDLE2(KINDLE):
PRODUCT_ID = [0x0002]
BCD = [0x0100]
def books(self, oncard=None, end_session=True):
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
# Read collections information
collections = os.path.join(self._main_prefix, 'system', 'collections.json')
if os.access(collections, os.R_OK):
try:
self.kindle_update_booklist(bl, collections)
except:
import traceback
traceback.print_exc()
return bl
def kindle_update_booklist(self, bl, collections):
with open(collections, 'rb') as f:
collections = f.read()
collections = json.loads(collections)
path_map = {}
for name, val in collections.items():
col = name.split('@')[0]
items = val.get('items', [])
for x in items:
x = x[-40:]
if x not in path_map:
path_map[x] = set([])
path_map[x].add(col)
if path_map:
for book in bl:
path = '/mnt/us/'+book.lpath
h = hashlib.sha1(path).hexdigest()
if h in path_map:
book.device_collections = list(sorted(path_map[h]))
class KINDLE_DX(KINDLE2):

View File

@ -30,7 +30,7 @@ def authors_to_string(authors):
def author_to_author_sort(author):
method = tweaks['author_sort_copy_method']
if method == 'copy' or (method == 'comma' and author.count(',') > 0):
if method == 'copy' or (method == 'comma' and ',' in author):
return author
tokens = author.split()
tokens = tokens[-1:] + tokens[:-1]

View File

@ -10,12 +10,31 @@ import os
from contextlib import closing
from calibre.customize import FileTypePlugin
from calibre.utils.zipfile import ZipFile, stringFileHeader
def is_comic(list_of_names):
extensions = set([x.rpartition('.')[-1].lower() for x in list_of_names])
comic_extensions = set(['jpg', 'jpeg', 'png'])
return len(extensions - comic_extensions) == 0
def archive_type(stream):
try:
pos = stream.tell()
except:
pos = 0
id_ = stream.read(4)
ans = None
if id_ == stringFileHeader:
ans = 'zip'
elif id_.startswith('Rar'):
ans = 'rar'
try:
stream.seek(pos)
except:
pass
return ans
class ArchiveExtract(FileTypePlugin):
name = 'Archive Extract'
author = 'Kovid Goyal'
@ -31,7 +50,6 @@ class ArchiveExtract(FileTypePlugin):
if is_rar:
from calibre.libunrar import extract_member, names
else:
from calibre.utils.zipfile import ZipFile
zf = ZipFile(archive, 'r')
if is_rar:

View File

@ -741,7 +741,7 @@ class OPF(object):
def fset(self, val):
for tag in list(self.tags_path(self.metadata)):
self.metadata.remove(tag)
tag.getparent().remove(tag)
for tag in val:
elem = self.create_metadata_element('subject')
self.set_text(elem, unicode(tag))

View File

@ -100,7 +100,9 @@ def _config():
c.add_opt('tag_browser_hidden_categories', default=set(),
help=_('tag browser categories not to display'))
c.add_opt('gui_layout', choices=['wide', 'narrow'],
help=_('The layout of the user interface'), default='narrow')
help=_('The layout of the user interface'), default='wide')
c.add_opt('show_avg_rating', default=True,
help=_('Show the average rating per item indication in the tag browser'))
return ConfigProxy(c)
config = _config()

View File

@ -8,73 +8,18 @@ __docformat__ = 'restructuredtext en'
import os, collections
from PyQt4.Qt import QLabel, QPixmap, QSize, QWidget, Qt, pyqtSignal, \
QVBoxLayout, QScrollArea
QVBoxLayout, QScrollArea, QPropertyAnimation, QEasingCurve, \
QSizePolicy, QPainter, QRect, pyqtProperty, QDesktopServices, QUrl
from calibre import fit_image, prepare_string_for_xml
from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import preferred_encoding
from calibre.library.comments import comments_to_html
class CoverView(QLabel):
def __init__(self, parent=None):
QLabel.__init__(self, parent)
self.default_pixmap = QPixmap(I('book.svg'))
self.max_width, self.max_height = 120, 120
self.setScaledContents(True)
self.setPixmap(self.default_pixmap)
def do_layout(self):
pixmap = self.pixmap()
pwidth, pheight = pixmap.width(), pixmap.height()
width, height = fit_image(pwidth, pheight,
self.max_width, self.max_height)[1:]
self.setMaximumWidth(width)
try:
aspect_ratio = pwidth/float(pheight)
except ZeroDivisionError:
aspect_ratio = 1
mh = min(self.max_height, int(width/aspect_ratio))
self.setMaximumHeight(mh)
def setPixmap(self, pixmap):
QLabel.setPixmap(self, pixmap)
self.do_layout()
def sizeHint(self):
return QSize(self.maximumWidth(), self.maximumHeight())
def relayout(self, parent_size):
self.max_height = int(parent_size.height()/3.)
self.max_width = parent_size.width()
self.do_layout()
def show_data(self, data):
if data.has_key('cover'):
self.setPixmap(QPixmap.fromImage(data.pop('cover')))
else:
self.setPixmap(self.default_pixmap)
class BookInfo(QScrollArea):
def __init__(self, parent=None):
QScrollArea.__init__(self, parent)
self.setWidgetResizable(True)
self.label = QLabel()
self.label.setWordWrap(True)
self.setWidget(self.label)
def show_data(self, data):
self.label.setText('')
self.data = data.copy()
rows = render_rows(self.data)
rows = u'\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
k, t in rows])
self.label.setText(u'<table>%s</table>'%rows)
# render_rows(data) {{{
WEIGHTS = collections.defaultdict(lambda : 100)
WEIGHTS[_('Path')] = 0
WEIGHTS[_('Path')] = 5
WEIGHTS[_('Formats')] = 1
WEIGHTS[_('Collections')] = 2
WEIGHTS[_('Series')] = 3
@ -86,7 +31,7 @@ def render_rows(data):
rows = []
for key in keys:
txt = data[key]
if key in ('id', _('Comments')) or not txt or not txt.strip() or \
if key in ('id', _('Comments')) or not hasattr(txt, 'strip') or not txt.strip() or \
txt == 'None':
continue
if isinstance(key, str):
@ -97,20 +42,164 @@ def render_rows(data):
txt = prepare_string_for_xml(txt)
if 'id' in data:
if key == _('Path'):
txt = '...'+os.sep+os.sep.join(txt.split(os.sep)[-2:])
txt = u'<a href="path:%s">%s</a>'%(data['id'], txt)
txt = u'<a href="path:%s" title="%s">%s</a>'%(data['id'],
txt, _('Click to open'))
if key == _('Formats') and txt and txt != _('None'):
fmts = [x.strip() for x in txt.split(',')]
fmts = [u'<a href="format:%s:%s">%s</a>' % (data['id'], x, x) for x
in fmts]
txt = ', '.join(fmts)
else:
if key == _('Path'):
txt = u'<a href="devpath:%s">%s</a>'%(txt,
_('Click to open'))
rows.append((key, txt))
return rows
# }}}
class CoverView(QWidget): # {{{
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setMaximumSize(QSize(120, 120))
self.setMinimumSize(QSize(120, 1))
self._current_pixmap_size = self.maximumSize()
self.animation = QPropertyAnimation(self, 'current_pixmap_size', self)
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
self.animation.setDuration(1000)
self.animation.setStartValue(QSize(0, 0))
self.animation.valueChanged.connect(self.value_changed)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.default_pixmap = QPixmap(I('book.svg'))
self.pixmap = self.default_pixmap
self.pwidth = self.pheight = None
self.data = {}
self.do_layout()
def value_changed(self, val):
self.update()
def setCurrentPixmapSize(self, val):
self._current_pixmap_size = val
def do_layout(self):
pixmap = self.pixmap
pwidth, pheight = pixmap.width(), pixmap.height()
self.pwidth, self.pheight = fit_image(pwidth, pheight,
self.rect().width(), self.rect().height())[1:]
self.current_pixmap_size = QSize(self.pwidth, self.pheight)
self.animation.setEndValue(self.current_pixmap_size)
def relayout(self, parent_size):
self.setMaximumSize(parent_size.width(),
min(int(parent_size.height()/2.),int(4/3. * parent_size.width())+1))
self.resize(self.maximumSize())
self.animation.stop()
self.do_layout()
def sizeHint(self):
return self.maximumSize()
def show_data(self, data):
self.animation.stop()
if data.get('id', None) == self.data.get('id', None):
return
self.data = {'id':data.get('id', None)}
if data.has_key('cover'):
self.pixmap = QPixmap.fromImage(data.pop('cover'))
if self.pixmap.isNull():
self.pixmap = self.default_pixmap
else:
self.pixmap = self.default_pixmap
self.do_layout()
self.update()
self.animation.start()
def paintEvent(self, event):
canvas_size = self.rect()
width = self.current_pixmap_size.width()
extrax = canvas_size.width() - width
if extrax < 0: extrax = 0
x = int(extrax/2.)
height = self.current_pixmap_size.height()
extray = canvas_size.height() - height
if extray < 0: extray = 0
y = int(extray/2.)
target = QRect(x, y, width, height)
p = QPainter(self)
p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
p.drawPixmap(target, self.pixmap.scaled(target.size(),
Qt.KeepAspectRatio, Qt.SmoothTransformation))
p.end()
current_pixmap_size = pyqtProperty('QSize',
fget=lambda self: self._current_pixmap_size,
fset=setCurrentPixmapSize
)
# }}}
class Label(QLabel):
mr = pyqtSignal(object)
link_clicked = pyqtSignal(object)
def __init__(self):
QLabel.__init__(self)
self.setTextFormat(Qt.RichText)
self.setText('')
self.setWordWrap(True)
self.linkActivated.connect(self.link_activated)
self._link_clicked = False
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
def link_activated(self, link):
self._link_clicked = True
link = unicode(link)
self.link_clicked.emit(link)
def mouseReleaseEvent(self, ev):
QLabel.mouseReleaseEvent(self, ev)
if not self._link_clicked:
self.mr.emit(ev)
self._link_clicked = False
class BookInfo(QScrollArea):
def __init__(self, parent=None):
QScrollArea.__init__(self, parent)
self.setWidgetResizable(True)
self.label = Label()
self.setWidget(self.label)
self.link_clicked = self.label.link_clicked
self.mr = self.label.mr
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def show_data(self, data):
self.label.setText('')
rows = render_rows(data)
rows = u'\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
k, t in rows])
if _('Comments') in data and data[_('Comments')]:
comments = comments_to_html(data[_('Comments')])
rows += u'<tr><td colspan="2">%s</td></tr>'%comments
self.label.setText(u'<table>%s</table>'%rows)
class BookDetails(QWidget):
resized = pyqtSignal(object)
show_book_info = pyqtSignal()
open_containing_folder = pyqtSignal(int)
view_specific_format = pyqtSignal(int, object)
# Drag 'n drop {{{
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
@ -151,21 +240,45 @@ class BookDetails(QWidget):
self.setLayout(self._layout)
self.cover_view = CoverView(self)
self.cover_view.relayout()
self.cover_view.relayout(self.size())
self.resized.connect(self.cover_view.relayout, type=Qt.QueuedConnection)
self._layout.addWidget(self.cover_view)
self._layout.addWidget(self.cover_view, alignment=Qt.AlignHCenter)
self.book_info = BookInfo(self)
self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self._link_clicked)
self.book_info.mr.connect(self.mouseReleaseEvent)
self.setMinimumSize(QSize(190, 200))
self.setCursor(Qt.PointingHandCursor)
def _link_clicked(self, link):
typ, _, val = link.partition(':')
if typ == 'path':
self.open_containing_folder.emit(int(val))
elif typ == 'format':
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
path = os.path.dirname(val)
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
def mouseReleaseEvent(self, ev):
ev.accept()
self.show_book_info.emit()
def resizeEvent(self, ev):
self.resized.emit(self.size())
def show_data(self, data):
self.cover_view.show_data(data)
self.book_info.show_data(data)
self.setToolTip('<p>'+_('Click to open Book Details window') +
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), ''))
def reset_info(self):
self.show_data({})
def mouseReleaseEvent(self, ev):
self.show_book_info.emit()

View File

@ -13,7 +13,7 @@ from PyQt4.Qt import QPixmap, SIGNAL
from calibre.gui2 import choose_images, error_dialog
from calibre.gui2.convert.metadata_ui import Ui_Form
from calibre.ebooks.metadata import authors_to_string, string_to_authors, \
MetaInformation, authors_to_sort_string
MetaInformation
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2.convert import Widget
@ -57,7 +57,7 @@ class MetadataWidget(Widget, Ui_Form):
au = unicode(self.author.currentText())
au = re.sub(r'\s+et al\.$', '', au)
authors = string_to_authors(au)
self.author_sort.setText(authors_to_sort_string(authors))
self.author_sort.setText(self.db.author_sort_from_authors(authors))
def initialize_metadata_options(self):

View File

@ -23,7 +23,7 @@ from calibre.devices.scanner import DeviceScanner
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
pixmap_to_data, warning_dialog, \
question_dialog, info_dialog, choose_dir
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
from calibre.ebooks.metadata import authors_to_string
from calibre import preferred_encoding, prints
from calibre.utils.filenames import ascii_filename
from calibre.devices.errors import FreeSpaceError
@ -1409,7 +1409,7 @@ class DeviceMixin(object): # {{{
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:
book.author_sort = authors_to_sort_string(book.authors)
book.author_sort = self.db.author_sort_from_authors(book.authors)
resend_metadata = True
if resend_metadata:

View File

@ -481,8 +481,18 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
self.device_detection_button.clicked.connect(self.debug_device_detection)
self.port.editingFinished.connect(self.check_port_value)
self.search_as_you_type.setChecked(config['search_as_you_type'])
self.show_avg_rating.setChecked(config['show_avg_rating'])
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
True))
li = None
for i, z in enumerate([('wide', _('Wide')),
('narrow', _('Narrow'))]):
x, y = z
self.opt_gui_layout.addItem(y, QVariant(x))
if x == config['gui_layout']:
li = i
self.opt_gui_layout.setCurrentIndex(li)
def check_port_value(self, *args):
port = self.port.value()
@ -854,6 +864,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked()
config['search_as_you_type'] = self.search_as_you_type.isChecked()
config['show_avg_rating'] = self.show_avg_rating.isChecked()
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
@ -863,6 +874,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
if self.viewer.item(i).checkState() == Qt.Checked:
fmts.append(str(self.viewer.item(i).text()))
config['internally_viewed_formats'] = fmts
val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString()
config['gui_layout'] = unicode(val)
if not path or not os.path.exists(path) or not os.path.isdir(path):
d = error_dialog(self, _('Invalid database location'),

View File

@ -89,8 +89,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>608</width>
<height>683</height>
<width>604</width>
<height>679</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_7">
@ -332,7 +332,7 @@
</widget>
<widget class="QWidget" name="page">
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<item row="1" column="0">
<widget class="QCheckBox" name="roman_numerals">
<property name="text">
<string>Use &amp;Roman numerals for series number</string>
@ -342,35 +342,45 @@
</property>
</widget>
</item>
<item row="1" column="0">
<item row="2" column="0">
<widget class="QCheckBox" name="systray_icon">
<property name="text">
<string>Enable system &amp;tray icon (needs restart)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="2" column="1">
<widget class="QCheckBox" name="systray_notifications">
<property name="text">
<string>Show &amp;notifications in system tray</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="show_splash_screen">
<property name="text">
<string>Show &amp;splash screen at startup</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="separate_cover_flow">
<property name="text">
<string>Show cover &amp;browser in a separate window (needs restart)</string>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QCheckBox" name="show_avg_rating">
<property name="text">
<string>Show &amp;average ratings in the tags browser</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="search_as_you_type">
<property name="text">
<string>Search as you type</string>
@ -380,21 +390,21 @@
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<item row="7" 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="6" column="0" colspan="2">
<item row="8" 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="7" column="0" colspan="2">
<item row="9" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_6">
@ -411,7 +421,7 @@
</item>
</layout>
</item>
<item row="8" column="0" colspan="2">
<item row="10" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Toolbar</string>
@ -459,7 +469,7 @@
</layout>
</widget>
</item>
<item row="9" column="0" colspan="2">
<item row="11" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QGroupBox" name="groupBox">
@ -625,6 +635,26 @@
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>User Interface &amp;layout (needs restart):</string>
</property>
<property name="buddy">
<cstring>opt_gui_layout</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_gui_layout">
<property name="maximumSize">
<size>
<width>250</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_6">

View File

@ -0,0 +1,82 @@
#!/usr/bin/env python
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView
from calibre.ebooks.metadata import author_to_author_sort
from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog
class tableItem(QTableWidgetItem):
def __ge__(self, other):
return unicode(self.text()).lower() >= unicode(other.text()).lower()
def __lt__(self, other):
return unicode(self.text()).lower() < unicode(other.text()).lower()
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
def __init__(self, parent, db, id_to_select):
QDialog.__init__(self, parent)
Ui_EditAuthorsDialog.__init__(self)
self.setupUi(self)
self.buttonBox.accepted.connect(self.accepted)
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
self.table.setColumnCount(2)
self.table.setHorizontalHeaderLabels([_('Author'), _('Author sort')])
self.authors = {}
auts = db.get_authors_with_ids()
self.table.setRowCount(len(auts))
select_item = None
for row, (id, author, sort) in enumerate(auts):
author = author.replace('|', ',')
self.authors[id] = (author, sort)
aut = tableItem(author)
aut.setData(Qt.UserRole, id)
sort = tableItem(sort)
self.table.setItem(row, 0, aut)
self.table.setItem(row, 1, sort)
if id == id_to_select:
select_item = sort
self.table.resizeColumnsToContents()
# set up the signal after the table is filled
self.table.cellChanged.connect(self.cell_changed)
self.table.setSortingEnabled(True)
self.table.sortByColumn(1, Qt.AscendingOrder)
if select_item is not None:
self.table.setCurrentItem(select_item)
self.table.editItem(select_item)
else:
self.table.setCurrentCell(0, 0)
def accepted(self):
self.result = []
for row in range(0,self.table.rowCount()):
id = self.table.item(row, 0).data(Qt.UserRole).toInt()[0]
aut = unicode(self.table.item(row, 0).text()).strip()
sort = unicode(self.table.item(row, 1).text()).strip()
orig_aut,orig_sort = self.authors[id]
if orig_aut != aut or orig_sort != sort:
self.result.append((id, orig_aut, aut, sort))
def cell_changed(self, row, col):
if col == 0:
item = self.table.item(row, 0)
aut = unicode(item.text()).strip()
c = self.table.item(row, 1)
c.setText(author_to_author_sort(aut))
item = c
else:
item = self.table.item(row, 1)
self.table.setCurrentItem(item)
# disable and reenable sorting to force the sort now, so we can scroll
# to the item after it moves
self.table.setSortingEnabled(False)
self.table.setSortingEnabled(True)
self.table.scrollToItem(item)

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditAuthorsDialog</class>
<widget class="QDialog" name="EditAuthorsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>730</width>
<height>342</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Manage authors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableWidget" name="table">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="columnCount">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>EditAuthorsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>229</x>
<y>211</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>234</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>EditAuthorsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>297</x>
<y>217</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>234</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -8,7 +8,7 @@ from PyQt4.QtGui import QDialog, QGridLayout
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_sort_string, \
from calibre.ebooks.metadata import string_to_authors, \
authors_to_string
from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page
@ -110,10 +110,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
au = string_to_authors(au)
self.db.set_authors(id, au, notify=False)
if self.auto_author_sort.isChecked():
aut = self.db.authors(id, index_is_id=True)
aut = aut if aut else ''
aut = [a.strip().replace('|', ',') for a in aut.strip().split(',')]
x = authors_to_sort_string(aut)
x = self.db.author_sort_from_book(id, index_is_id=True)
if x:
self.db.set_author_sort(id, x, notify=False)
aus = unicode(self.author_sort.text())

View File

@ -23,7 +23,7 @@ from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, \
from calibre.ebooks.metadata import string_to_authors, \
authors_to_string, check_isbn
from calibre.ebooks.metadata.library_thing import cover_from_isbn
from calibre import islinux, isfreebsd
@ -460,7 +460,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
au = unicode(self.authors.text())
au = re.sub(r'\s+et al\.$', '', au)
authors = string_to_authors(au)
self.author_sort.setText(authors_to_sort_string(authors))
self.author_sort.setText(self.db.author_sort_from_authors(authors))
def swap_title_author(self):
title = self.title.text()

View File

@ -121,6 +121,9 @@
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>

View File

@ -17,6 +17,8 @@ from calibre.gui2 import config, is_widescreen
from calibre.gui2.library.views import BooksView, DeviceBooksView
from calibre.gui2.widgets import Splitter
from calibre.gui2.tag_view import TagBrowserWidget
from calibre.gui2.status import StatusBar, HStatusBar
from calibre.gui2.book_details import BookDetails
_keep_refs = []
@ -290,9 +292,9 @@ class LibraryViewMixin(object): # {{{
class LibraryWidget(Splitter): # {{{
def __init__(self, parent):
orientation = Qt.Vertical if config['gui_layout'] == 'narrow' and \
not is_widescreen() else Qt.Horizontal
#orientation = Qt.Vertical
orientation = Qt.Vertical
if config['gui_layout'] == 'narrow':
orientation = Qt.Horizontal if is_widescreen() else Qt.Vertical
idx = 0 if orientation == Qt.Vertical else 1
size = 300 if orientation == Qt.Vertical else 550
Splitter.__init__(self, 'cover_browser_splitter', _('Cover Browser'),
@ -360,7 +362,6 @@ class LayoutMixin(object): # {{{
self.setWindowTitle(__appname__)
if config['gui_layout'] == 'narrow':
from calibre.gui2.status import StatusBar
self.status_bar = self.book_details = StatusBar(self)
self.stack = Stack(self)
self.bd_splitter = Splitter('book_details_splitter',
@ -375,8 +376,28 @@ class LayoutMixin(object): # {{{
l.addWidget(self.sidebar)
self.bd_splitter.addWidget(self._layout_mem[0])
self.bd_splitter.addWidget(self.status_bar)
self.bd_splitter.setCollapsible((self.bd_splitter.side_index+1)%2, False)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
self.centralwidget.layout().addWidget(self.bd_splitter)
else:
self.status_bar = HStatusBar(self)
self.setStatusBar(self.status_bar)
self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.svg'), initial_side_size=200,
orientation=Qt.Horizontal, parent=self, side_index=1)
self.stack = Stack(self)
self.bd_splitter.addWidget(self.stack)
self.book_details = BookDetails(self)
self.bd_splitter.addWidget(self.book_details)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
self.bd_splitter.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Expanding))
self.centralwidget.layout().addWidget(self.bd_splitter)
for x in ('cb', 'tb', 'bd'):
button = getattr(self, x+'_splitter').button
button.setIconSize(QSize(22, 22))
self.status_bar.addPermanentWidget(button)
self.status_bar.addPermanentWidget(self.jobs_button)
def finalize_layout(self):
m = self.library_view.model()

View File

@ -274,11 +274,15 @@ class JobsButton(QFrame):
def __init__(self, horizontal=False, size=48, parent=None):
QFrame.__init__(self, parent)
if horizontal:
size = 24
self.pi = ProgressIndicator(self, size)
self._jobs = QLabel('<b>'+_('Jobs:')+' 0')
self._jobs.mouseReleaseEvent = self.mouseReleaseEvent
if horizontal:
self.setLayout(QHBoxLayout())
self.layout().setDirection(self.layout().RightToLeft)
else:
self.setLayout(QVBoxLayout())
self._jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom)

View File

@ -420,8 +420,11 @@ class BooksModel(QAbstractTableModel): # {{{
pt.orig_file_path = os.path.abspath(src.name)
pt.seek(0)
if set_metadata:
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
try:
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
format)
except:
traceback.print_exc()
pt.close()
def to_uni(x):
if isbytestring(x):

View File

@ -554,7 +554,7 @@ void PictureFlowPrivate::resize(int w, int h)
if (h < 10) h = 10;
slideHeight = int(float(h)/REFLECTION_FACTOR);
slideWidth = int(float(slideHeight) * 2/3.);
fontSize = MAX(int(h/20.), 12);
fontSize = MAX(int(h/15.), 12);
recalc(w, h);
resetSlides();
triggerRender();

View File

@ -5,7 +5,7 @@ import os
from PyQt4.Qt import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
QSizePolicy, QScrollArea, Qt, QSize, pyqtSignal, \
QPropertyAnimation, QEasingCurve
QPropertyAnimation, QEasingCurve, QDesktopServices, QUrl
from calibre import fit_image, preferred_encoding, isosx
@ -49,7 +49,7 @@ class BookInfoDisplay(QWidget):
event.acceptProposedAction()
class BookCoverDisplay(QLabel):
class BookCoverDisplay(QLabel): # {{{
def __init__(self, coverpath=I('book.svg')):
QLabel.__init__(self)
@ -90,6 +90,7 @@ class BookInfoDisplay(QWidget):
self.statusbar_height = statusbar_size.height()
self.do_layout()
# }}}
class BookDataDisplay(QLabel):
@ -151,7 +152,7 @@ class BookInfoDisplay(QWidget):
k, t in rows])
if _('Comments') in self.data:
comments = comments_to_html(self.data[_('Comments')])
comments = '<b>Comments:</b>'+comments
comments = ('<b>%s:</b>'%_('Comments'))+comments
left_pane = u'<table>%s</table>'%rows
right_pane = u'<div>%s</div>'%comments
self.book_data.setText(u'<table><tr><td valign="top" '
@ -162,6 +163,10 @@ class BookInfoDisplay(QWidget):
self.book_data.updateGeometry()
self.updateGeometry()
self.setVisible(True)
self.setToolTip('<p>'+_('Click to open Book Details window') +
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), ''))
class StatusBarInterface(object):
@ -196,6 +201,9 @@ class BookDetailsInterface(object):
def show_data(self, data):
raise NotImplementedError()
class HStatusBar(QStatusBar, StatusBarInterface):
pass
class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface):
files_dropped = pyqtSignal(object, object)
@ -227,9 +235,13 @@ class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface):
typ, _, val = link.partition(':')
if typ == 'path':
self.open_containing_folder.emit(int(val))
if typ == 'format':
elif typ == 'format':
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
path = os.path.dirname(val)
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
def resizeEvent(self, ev):
self.resized.emit(self.size())

View File

@ -13,15 +13,76 @@ from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
QPushButton, QWidget
QPushButton, QWidget, QItemDelegate, QString, QPen, \
QColor, QLinearGradient, QBrush
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.utils.config import prefs, tweaks
from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
class TagDelegate(QItemDelegate):
def __init__(self, parent):
QItemDelegate.__init__(self, parent)
self._parent = parent
self.icon = QIcon(I('star.png'))
def paint(self, painter, option, index):
item = index.internalPointer()
if item.type != TagTreeItem.TAG:
QItemDelegate.paint(self, painter, option, index)
return
r = option.rect
# Paint the decoration icon
icon = self._parent.model().data(index, Qt.DecorationRole).toPyObject()
icon.paint(painter, r, Qt.AlignLeft)
# Paint the rating, if any. The decoration icon is assumed to be square,
# filling the row top to bottom. The three is arbitrary, there to
# provide a little space between the icon and what follows
r.setLeft(r.left()+r.height()+3)
rating = item.tag.avg_rating
if config['show_avg_rating'] and item.tag.avg_rating is not None:
painter.save()
if tweaks['render_avg_rating_using'] == 'star':
painter.setClipRect(r.left(), r.top(),
int(r.height()*(rating/5.0)), r.height())
self.icon.paint(painter, r, Qt.AlignLeft | Qt.AlignVCenter)
r.setLeft(r.left() + r.height())
else:
painter.translate(r.left(), r.top())
# Compute factor so sizes can be expressed in percentages of the
# box defined by the row height
factor = r.height()/100.
width = 20
height = 80
left_offset = 5
top_offset = 10
if r > 0.0:
color = QColor(100, 100, 255) #medium blue, less glare
pen = QPen(color, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
painter.setPen(pen)
painter.scale(factor, factor)
painter.drawRect(left_offset, top_offset, width, height)
fill_height = height*(rating/5.0)
gradient = QLinearGradient(0, 0, 0, 100)
gradient.setColorAt(0.0, color)
gradient.setColorAt(1.0, color)
painter.setBrush(QBrush(gradient))
painter.drawRect(left_offset, top_offset+(height-fill_height),
width, fill_height)
# The '3' is arbitrary, there because we need a little space
# between the rectangle and the text.
r.setLeft(r.left() + ((width+left_offset*2)*factor) + 3)
painter.restore()
# Paint the text
painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter,
QString('[%d] %s'%(item.tag.count, item.tag.name)))
class TagsView(QTreeView): # {{{
@ -30,6 +91,7 @@ class TagsView(QTreeView): # {{{
user_category_edit = pyqtSignal(object)
tag_list_edit = pyqtSignal(object, object)
saved_search_edit = pyqtSignal(object)
author_sort_edit = pyqtSignal(object, object)
tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal()
@ -43,6 +105,7 @@ class TagsView(QTreeView): # {{{
self.setAlternatingRowColors(True)
self.setAnimated(True)
self.setHeaderHidden(True)
self.setItemDelegate(TagDelegate(self))
def set_database(self, db, tag_match, popularity):
self.hidden_categories = config['tag_browser_hidden_categories']
@ -112,6 +175,9 @@ class TagsView(QTreeView): # {{{
if action == 'manage_searches':
self.saved_search_edit.emit(category)
return
if action == 'edit_author_sort':
self.author_sort_edit.emit(self, index)
return
if action == 'hide':
self.hidden_categories.add(category)
elif action == 'show':
@ -132,6 +198,7 @@ class TagsView(QTreeView): # {{{
if item.type == TagTreeItem.TAG:
tag_item = item
tag_name = item.tag.name
tag_id = item.tag.id
item = item.parent
if item.type == TagTreeItem.CATEGORY:
category = unicode(item.name.toString())
@ -147,9 +214,13 @@ class TagsView(QTreeView): # {{{
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
self.db.field_metadata[key]['is_custom'] and \
self.db.field_metadata[key]['datatype'] != 'rating'):
self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
self.context_menu.addAction(_('Rename \'%s\'')%tag_name,
partial(self.context_menu_handler, action='edit_item',
category=tag_item, index=index))
if key == 'authors':
self.context_menu.addAction(_('Edit sort for \'%s\'')%tag_name,
partial(self.context_menu_handler,
action='edit_author_sort', index=tag_id))
self.context_menu.addSeparator()
# Hide/Show/Restore categories
self.context_menu.addAction(_('Hide category %s') % category,
@ -166,9 +237,12 @@ class TagsView(QTreeView): # {{{
self.context_menu.addSeparator()
if key in ['tags', 'publisher', 'series'] or \
self.db.field_metadata[key]['is_custom']:
self.context_menu.addAction(_('Manage ') + category,
self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='open_editor',
category=tag_name, key=key))
elif key == 'authors':
self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='edit_author_sort'))
elif key == 'search':
self.context_menu.addAction(_('Manage Saved Searches'),
partial(self.context_menu_handler, action='manage_searches',
@ -298,7 +372,11 @@ class TagTreeItem(object): # {{{
if self.tag.count == 0:
return QVariant('%s'%(self.tag.name))
else:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
if self.tag.avg_rating is None:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
else:
return QVariant('[%d][%3.1f] %s'%(self.tag.count,
self.tag.avg_rating, self.tag.name))
if role == Qt.EditRole:
return QVariant(self.tag.name)
if role == Qt.DecorationRole:
@ -332,6 +410,7 @@ class TagsModel(QAbstractItemModel): # {{{
':custom' : QIcon(I('column.svg')),
':user' : QIcon(I('drawer.svg')),
'search' : QIcon(I('search.svg'))})
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db
@ -354,7 +433,14 @@ class TagsModel(QAbstractItemModel): # {{{
data=self.categories[i],
category_icon=self.category_icon_map[r],
tooltip=tt, category_key=r)
# This duplicates code in refresh(). Having it here as well
# can save seconds during startup, because we avoid a second
# call to get_node_tree.
for tag in data[r]:
if r not in self.categories_with_ratings and \
not self.db.field_metadata[r]['is_custom'] and \
not self.db.field_metadata[r]['kind'] == 'user':
tag.avg_rating = None
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
def set_search_restriction(self, s):
@ -417,6 +503,10 @@ class TagsModel(QAbstractItemModel): # {{{
if len(data[r]) > 0:
self.beginInsertRows(category_index, 0, len(data[r])-1)
for tag in data[r]:
if r not in self.categories_with_ratings and \
not self.db.field_metadata[r]['is_custom'] and \
not self.db.field_metadata[r]['kind'] == 'user':
tag.avg_rating = None
tag.state = state_map.get(tag.name, 0)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
self.endInsertRows()
@ -607,6 +697,7 @@ class TagBrowserMixin(object): # {{{
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.tags_view.author_sort_edit.connect(self.do_author_sort_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
self.edit_categories.clicked.connect(lambda x:
@ -636,6 +727,19 @@ class TagBrowserMixin(object): # {{{
self.saved_search.clear_to_help()
self.search.clear_to_help()
def do_author_sort_edit(self, parent, id):
db = self.library_view.model().db
editor = EditAuthorsDialog(parent, db, id)
d = editor.exec_()
if d:
for (id, old_author, new_author, new_sort) in editor.result:
if old_author != new_author:
# The id might change if the new author already exists
id = db.rename_author(id, new_author)
db.set_sort_field_for_author(id, unicode(new_sort))
self.library_view.model().refresh()
self.tags_view.recount()
# }}}
class TagBrowserWidget(QWidget): # {{{

View File

@ -126,7 +126,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
# Jobs Button {{{
self.job_manager = JobManager()
self.jobs_dialog = JobsDialog(self, self.job_manager)
self.jobs_button = JobsButton()
self.jobs_button = JobsButton(horizontal=config['gui_layout'] !=
'narrow')
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
# }}}

View File

@ -1110,6 +1110,8 @@ class Splitter(QSplitter):
def show_side_pane(self):
if self.count() < 2 or not self.is_side_index_hidden:
return
if self.desired_side_size == 0:
self.desired_side_size = self.initial_side_size
self.apply_state((True, self.desired_side_size))
def hide_side_pane(self):

View File

@ -461,14 +461,27 @@ class CustomColumns(object):
CREATE VIEW tag_browser_{table} AS SELECT
id,
value,
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link as bl,
ratings as r
WHERE {lt}.value={table}.id and bl.book={lt}.book and
r.id = bl.rating and r.rating <> 0) avg_rating
FROM {table};
CREATE VIEW tag_browser_filtered_{table} AS SELECT
id,
value,
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
books_list_filter(book)) count
books_list_filter(book)) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link as bl,
ratings as r
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
FROM {table};
'''.format(lt=lt, table=table),
@ -505,7 +518,6 @@ class CustomColumns(object):
END;
'''.format(table=table),
]
script = ' \n'.join(lines)
self.conn.executescript(script)
self.conn.commit()

View File

@ -12,7 +12,7 @@ from math import floor
from PyQt4.QtGui import QImage
from calibre.ebooks.metadata import title_sort
from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.library.database import LibraryDatabase
from calibre.library.field_metadata import FieldMetadata, TagsIcons
from calibre.library.schema_upgrades import SchemaUpgrade
@ -20,7 +20,7 @@ from calibre.library.caches import ResultCache
from calibre.library.custom_columns import CustomColumns
from calibre.library.sqlite import connect, IntegrityError, DBThread
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
MetaInformation, authors_to_sort_string
MetaInformation
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile
@ -56,11 +56,14 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
class Tag(object):
def __init__(self, name, id=None, count=0, state=0, tooltip=None, icon=None):
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
tooltip=None, icon=None):
self.name = name
self.id = id
self.count = count
self.state = state
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
self.tooltip = tooltip
self.icon = icon
@ -133,7 +136,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
id,
name,
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count,
(0) as avg_rating,
name as sort
FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
@ -144,7 +149,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_news AS SELECT DISTINCT
id,
name,
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count,
(0) as avg_rating,
name as sort
FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
@ -422,6 +429,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
mi.authors_sort_strings = self.authors_sort_strings(idx, index_is_id)
mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
@ -698,13 +706,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
continue
cn = cat['column']
if ids is None:
query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn, tn)
query = '''SELECT id, {0}, count, avg_rating, sort
FROM tag_browser_{1}'''.format(cn, tn)
else:
query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn, tn)
query = '''SELECT id, {0}, count, avg_rating, sort
FROM tag_browser_filtered_{1}'''.format(cn, tn)
if sort_on_count:
query += ' ORDER BY count DESC'
else:
query += ' ORDER BY {0} ASC'.format(cn)
query += ' ORDER BY sort ASC'
data = self.conn.get(query)
# icon_map is not None if get_categories is to store an icon and
@ -722,6 +732,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
datatype = cat['datatype']
if datatype == 'rating':
# eliminate the zero ratings line as well as count == 0
item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
formatter = (lambda x:u'\u2605'*int(round(x/2.)))
elif category == 'authors':
@ -733,15 +744,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
formatter = (lambda x:unicode(x))
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
icon=icon, tooltip = tooltip)
avg=r[3], sort=r[4],
icon=icon, tooltip=tooltip)
for r in data if item_not_zero_func(r)]
if category == 'series' and not sort_on_count:
if tweaks['title_series_sorting'] == 'library_order':
ts = lambda x: title_sort(x)
else:
ts = lambda x:x
categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(),
ts(y.name).lower()))
# We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically
@ -909,6 +914,38 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.set_path(id, True)
self.notify('metadata', [id])
# Given a book, return the list of author sort strings for the book's authors
def authors_sort_strings(self, id, index_is_id=False):
id = id if index_is_id else self.id(id)
aut_strings = self.conn.get('''
SELECT sort
FROM authors, books_authors_link as bl
WHERE bl.book=? and authors.id=bl.author
ORDER BY bl.id''', (id,))
result = []
for (sort,) in aut_strings:
result.append(sort)
return result
# Given a book, return the author_sort string for authors of the book
def author_sort_from_book(self, id, index_is_id=False):
auts = self.authors_sort_strings(id, index_is_id)
return ' & '.join(auts).replace('|', ',')
# Given a list of authors, return the author_sort string for the authors,
# preferring the author sort associated with the author over the computed
# string
def author_sort_from_authors(self, authors):
result = []
for aut in authors:
r = self.conn.get('SELECT sort FROM authors WHERE name=?',
(aut.replace(',', '|'),), all=False)
if r is None:
result.append(author_to_author_sort(aut))
else:
result.append(r)
return ' & '.join(result).replace('|', ',')
def set_authors(self, id, authors, notify=True):
'''
`authors`: A list of authors.
@ -935,7 +972,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
(id, aid))
except IntegrityError: # Sometimes books specify the same author twice in their metadata
pass
ss = authors_to_sort_string(authors)
self.conn.commit()
ss = self.author_sort_from_book(id, index_is_id=True)
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
(ss, id))
self.conn.commit()
@ -1007,6 +1045,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
def rename_tag(self, old_id, new_name):
new_name = new_name.strip()
new_id = self.conn.get(
'''SELECT id from tags
WHERE name=?''', (new_name,), all=False)
@ -1046,6 +1085,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
def rename_series(self, old_id, new_name):
new_name = new_name.strip()
new_id = self.conn.get(
'''SELECT id from series
WHERE name=?''', (new_name,), all=False)
@ -1075,7 +1115,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
index = index + 1
self.conn.commit()
def delete_series_using_id(self, id):
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,))
self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,))
@ -1091,6 +1130,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
def rename_publisher(self, old_id, new_name):
new_name = new_name.strip()
new_id = self.conn.get(
'''SELECT id from publishers
WHERE name=?''', (new_name,), all=False)
@ -1113,12 +1153,25 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,))
self.conn.commit()
# There is no editor for author, so we do not need get_authors_with_ids or
# delete_author_using_id.
def get_authors_with_ids(self):
result = self.conn.get('SELECT id,name,sort FROM authors')
if not result:
return []
return result
def set_sort_field_for_author(self, old_id, new_sort):
self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
(new_sort.strip(), old_id))
self.conn.commit()
# Now change all the author_sort fields in books by this author
bks = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,))
for (book_id,) in bks:
ss = self.author_sort_from_book(book_id, index_is_id=True)
self.set_author_sort(book_id, ss)
def rename_author(self, old_id, new_name):
# Make sure that any commas in new_name are changed to '|'!
new_name = new_name.replace(',', '|')
new_name = new_name.replace(',', '|').strip()
# Get the list of books we must fix up, one way or the other
# Save the list so we can use it twice
@ -1141,7 +1194,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('UPDATE authors SET name=? WHERE id=?',
(new_name, old_id))
self.conn.commit()
return
return new_id
# Author exists. To fix this, we must replace all the authors
# instead of replacing the one. Reason: db integrity checks can stop
# the rename process, which would leave everything half-done. We
@ -1184,24 +1237,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# now fix the filesystem paths
self.set_path(book_id, index_is_id=True)
# Next fix the author sort. Reset it to the default
authors = self.conn.get('''
SELECT authors.name
FROM authors, books_authors_link as bl
WHERE bl.book = ? and bl.author = authors.id
ORDER BY bl.id
''' , (book_id,))
# unpack the double-list structure
for i,aut in enumerate(authors):
authors[i] = aut[0]
ss = authors_to_sort_string(authors)
# Change the '|'s to ','
ss = ss.replace('|', ',')
self.conn.execute('''UPDATE books
SET author_sort=?
WHERE id=?''', (ss, book_id))
self.conn.commit()
ss = self.author_sort_from_book(book_id, index_is_id=True)
self.set_author_sort(book_id, ss)
# the caller will do a general refresh, so we don't need to
# do one here
return new_id
# end convenience methods
@ -1436,7 +1476,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if not add_duplicates and self.has_book(mi):
return None
series_index = 1.0 if mi.series_index is None else mi.series_index
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
title = mi.title
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
@ -1476,7 +1516,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
duplicates.append((path, format, mi))
continue
series_index = 1.0 if mi.series_index is None else mi.series_index
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
title = mi.title
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
@ -1515,7 +1555,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.title = _('Unknown')
if not mi.authors:
mi.authors = [_('Unknown')]
aus = mi.author_sort if mi.author_sort else authors_to_sort_string(mi.authors)
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
title = mi.title if isinstance(mi.title, unicode) else \

View File

@ -44,9 +44,12 @@ class FieldMetadata(dict):
is_category: is a tag browser category. If true, then:
table: name of the db table used to construct item list
column: name of the column in the normalized table to join on
link_column: name of the column in the connection table to join on
link_column: name of the column in the connection table to join on. This
key should not be present if there is no link table
category_sort: the field in the normalized table to sort on. This
key must be present if is_category is True
If these are None, then the category constructor must know how
to build the item list (e.g., formats).
to build the item list (e.g., formats, news).
The order below is the order that the categories will
appear in the tags pane.
@ -66,6 +69,7 @@ class FieldMetadata(dict):
('authors', {'table':'authors',
'column':'name',
'link_column':'author',
'category_sort':'sort',
'datatype':'text',
'is_multiple':',',
'kind':'field',
@ -76,6 +80,7 @@ class FieldMetadata(dict):
('series', {'table':'series',
'column':'name',
'link_column':'series',
'category_sort':'(title_sort(name))',
'datatype':'text',
'is_multiple':None,
'kind':'field',
@ -95,6 +100,7 @@ class FieldMetadata(dict):
('publisher', {'table':'publishers',
'column':'name',
'link_column':'publisher',
'category_sort':'name',
'datatype':'text',
'is_multiple':None,
'kind':'field',
@ -105,6 +111,7 @@ class FieldMetadata(dict):
('rating', {'table':'ratings',
'column':'rating',
'link_column':'rating',
'category_sort':'rating',
'datatype':'rating',
'is_multiple':None,
'kind':'field',
@ -114,6 +121,7 @@ class FieldMetadata(dict):
'is_category':True}),
('news', {'table':'news',
'column':'name',
'category_sort':'name',
'datatype':None,
'is_multiple':None,
'kind':'category',
@ -124,6 +132,7 @@ class FieldMetadata(dict):
('tags', {'table':'tags',
'column':'name',
'link_column': 'tag',
'category_sort':'name',
'datatype':'text',
'is_multiple':',',
'kind':'field',
@ -374,7 +383,7 @@ class FieldMetadata(dict):
'search_terms':[key], 'label':label,
'colnum':colnum, 'display':display,
'is_custom':True, 'is_category':is_category,
'link_column':'value',
'link_column':'value','category_sort':'value',
'is_editable': is_editable,}
self._add_search_terms_to_map(key, [key])
self.custom_label_to_key_map[label] = key

View File

@ -291,4 +291,122 @@ class SchemaUpgrade(object):
for field in self.field_metadata.itervalues():
if field['is_category'] and not field['is_custom'] and 'link_column' in field:
create_tag_browser_view(field['table'], field['link_column'], field['column'])
table = self.conn.get(
'SELECT name FROM sqlite_master WHERE type="table" AND name=?',
('books_%s_link'%field['table'],), all=False)
if table is not None:
create_tag_browser_view(field['table'], field['link_column'], field['column'])
def upgrade_version_11(self):
'Add average rating to tag browser views'
def create_std_tag_browser_view(table_name, column_name,
view_column_name, sort_column_name):
script = ('''
DROP VIEW IF EXISTS tag_browser_{tn};
CREATE VIEW tag_browser_{tn} AS SELECT
id,
{vcn},
(SELECT COUNT(id) FROM books_{tn}_link WHERE {cn}={tn}.id) count,
(SELECT AVG(ratings.rating)
FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings
WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND
ratings.id = bl.rating AND ratings.rating <> 0) avg_rating,
{scn} AS sort
FROM {tn};
DROP VIEW IF EXISTS tag_browser_filtered_{tn};
CREATE VIEW tag_browser_filtered_{tn} AS SELECT
id,
{vcn},
(SELECT COUNT(books_{tn}_link.id) FROM books_{tn}_link WHERE
{cn}={tn}.id AND books_list_filter(book)) count,
(SELECT AVG(ratings.rating)
FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings
WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND
ratings.id = bl.rating AND ratings.rating <> 0 AND
books_list_filter(bl.book)) avg_rating,
{scn} AS sort
FROM {tn};
'''.format(tn=table_name, cn=column_name,
vcn=view_column_name, scn= sort_column_name))
self.conn.executescript(script)
def create_cust_tag_browser_view(table_name, link_table_name):
script = '''
DROP VIEW IF EXISTS tag_browser_{table};
CREATE VIEW tag_browser_{table} AS SELECT
id,
value,
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link AS bl,
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
FROM {table};
DROP VIEW IF EXISTS tag_browser_filtered_{table};
CREATE VIEW tag_browser_filtered_{table} AS SELECT
id,
value,
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
books_list_filter(book)) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link AS bl,
ratings AS r
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
FROM {table};
'''.format(lt=link_table_name, table=table_name)
self.conn.executescript(script)
for field in self.field_metadata.itervalues():
if field['is_category'] and not field['is_custom'] and 'link_column' in field:
table = self.conn.get(
'SELECT name FROM sqlite_master WHERE type="table" AND name=?',
('books_%s_link'%field['table'],), all=False)
if table is not None:
create_std_tag_browser_view(field['table'], field['link_column'],
field['column'], field['category_sort'])
db_tables = self.conn.get('''SELECT name FROM sqlite_master
WHERE type='table'
ORDER BY name''');
tables = []
for (table,) in db_tables:
tables.append(table)
for table in tables:
link_table = 'books_%s_link'%table
if table.startswith('custom_column_') and link_table in tables:
create_cust_tag_browser_view(table, link_table)
from calibre.ebooks.metadata import author_to_author_sort
aut = self.conn.get('SELECT id, name FROM authors');
records = []
for (id, author) in aut:
records.append((id, author.replace('|', ',')))
for id,author in records:
self.conn.execute('UPDATE authors SET sort=? WHERE id=?',
(author_to_author_sort(author.replace('|', ',')).strip(), id))
self.conn.commit()
self.conn.executescript('''
DROP TRIGGER IF EXISTS author_insert_trg;
CREATE TRIGGER author_insert_trg
AFTER INSERT ON authors
BEGIN
UPDATE authors SET sort=author_to_author_sort(NEW.name) WHERE id=NEW.id;
END;
DROP TRIGGER IF EXISTS author_update_trg;
CREATE TRIGGER author_update_trg
BEFORE UPDATE ON authors
BEGIN
UPDATE authors SET sort=author_to_author_sort(NEW.name)
WHERE id=NEW.id AND name <> NEW.name;
END;
''')

View File

@ -14,7 +14,7 @@ from Queue import Queue
from threading import RLock
from datetime import datetime
from calibre.ebooks.metadata import title_sort
from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, isoformat
@ -116,10 +116,12 @@ class DBThread(Thread):
self.conn.create_aggregate('concat', 1, Concatenate)
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
if tweaks['title_series_sorting'] == 'library_order':
self.conn.create_function('title_sort', 1, title_sort)
else:
if tweaks['title_series_sorting'] == 'strictly_alphabetic':
self.conn.create_function('title_sort', 1, lambda x:x)
else:
self.conn.create_function('title_sort', 1, title_sort)
self.conn.create_function('author_to_author_sort', 1,
lambda x: author_to_author_sort(x.replace('|', ',')))
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
# Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1)

File diff suppressed because it is too large Load Diff