mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sync to trunk.
This commit is contained in:
commit
0f62ea111c
@ -19,6 +19,65 @@
|
||||
# new recipes:
|
||||
# - title:
|
||||
|
||||
|
||||
- version: 0.7.54
|
||||
date: 2011-04-08
|
||||
|
||||
new features:
|
||||
- title: "New output format, HTMLZ which is a single HTML file with its associated images/stylesheets in a zipped up file"
|
||||
description: "Useful when you want to convert your ebook into a single HTML file for easy editing. Note that this output plugin is still new and needs testing"
|
||||
|
||||
- title: "When dealing with ZIP/RAR archives, use the file header rather than the file extension to detrmine the file type, when possible. This fixes the common case of CBZ files being actually cbr files and vice versa"
|
||||
|
||||
- title: "Support for the Motorola Atrix"
|
||||
|
||||
- title: "Allow the icons in the toolbar to be turned off completely via Preferences->Look & Feel"
|
||||
|
||||
- title: "When downloading metadata use the gzip transfer encoding when possible for a speedup."
|
||||
tickets: [749304]
|
||||
|
||||
bug fixes:
|
||||
- title: "Conversion pipeline: Workaround for bug in lxml that causes a massive mem leak on windows and OS X when the input document contains non ASCII CSS selectors."
|
||||
tickets: [754555]
|
||||
|
||||
- title: "Conversion pipeline: Handle inline <style> tags that put all the actual CSS inside an XML comment."
|
||||
tickets: [750063]
|
||||
|
||||
- title: "The 'Choose Library' button now shows its popup menu when you already have more than one library instead of the dialog to create a new library"
|
||||
tickets: [754154]
|
||||
|
||||
- title: "Apply all content server setting when clicking the Start Server button in Preferences->Sharing over the net"
|
||||
tickets: [753122]
|
||||
|
||||
- title: "Fix content server breaking if its restriction is set to a saved search that was deleted"
|
||||
tickets: [751950]
|
||||
|
||||
- title: "Fix detection of PocketBook with 2.0.6 firmware on windows"
|
||||
tickets: [750336]
|
||||
|
||||
- title: "ODT Input: Fix handling of the <text:s> element."
|
||||
tickets: [749655]
|
||||
|
||||
- title: "MOBI Output: Don't use self closed tags"
|
||||
|
||||
- title: "Fix book details popup becoming too tall if there is a lot of metadata"
|
||||
|
||||
- title: "Fix new PDF engine crashing on PDF files with embedded fonts with null names"
|
||||
|
||||
improved recipes:
|
||||
- Kommersant
|
||||
- Perfil
|
||||
- Times of India
|
||||
- IHT
|
||||
- Guardian
|
||||
|
||||
new recipes:
|
||||
- title: "Al Ahram"
|
||||
authors: Hassan Williamson
|
||||
|
||||
- title: "F-Secure and developpez.com"
|
||||
authors: louhike
|
||||
|
||||
- version: 0.7.53
|
||||
date: 2011-04-01
|
||||
|
||||
|
62
recipes/al_ahram.recipe
Normal file
62
recipes/al_ahram.recipe
Normal file
@ -0,0 +1,62 @@
|
||||
# coding=utf-8
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Hassan Williamson <haz at hazrpg.co.uk>'
|
||||
'''
|
||||
ahram.org.eg
|
||||
'''
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class AlAhram(BasicNewsRecipe):
|
||||
title = 'Al-Ahram'
|
||||
__author__ = 'Hassan Williamson'
|
||||
description = 'News from Egypt in Arabic.'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
#delay = 1
|
||||
use_embedded_content = False
|
||||
encoding = 'utf8'
|
||||
publisher = 'Al-Ahram'
|
||||
category = 'News'
|
||||
language = 'ar'
|
||||
publication_type = 'newsportal'
|
||||
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif; direction: rtl; } .txtTitle{ font-weight: bold; } '
|
||||
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':['bbcolright']})
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['bbnav', 'bbsp']}),
|
||||
dict(name='div', attrs={'id':['AddThisButton']})
|
||||
]
|
||||
|
||||
remove_attributes = [
|
||||
'width','height'
|
||||
]
|
||||
|
||||
feeds = [
|
||||
(u'الأولى', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=25'),
|
||||
(u'مصر', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=27'),
|
||||
(u'المحافظات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=29'),
|
||||
(u'الوطن العربي', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=31'),
|
||||
(u'العالم', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=26'),
|
||||
(u'تقارير المراسلين', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=2'),
|
||||
(u'تحقيقات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=3'),
|
||||
(u'قضايا واراء', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=4'),
|
||||
(u'اقتصاد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=5'),
|
||||
(u'رياضة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=6'),
|
||||
(u'حوادث', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=38'),
|
||||
(u'دنيا الثقافة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=7'),
|
||||
(u'المراة والطفل', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=8'),
|
||||
(u'يوم جديد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=9'),
|
||||
(u'الكتاب', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=10'),
|
||||
(u'الاعمدة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=11'),
|
||||
(u'أراء حرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=59'),
|
||||
(u'ملفات الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=12'),
|
||||
(u'بريد الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=15'),
|
||||
(u'الاخيرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=16'),
|
||||
]
|
||||
|
||||
|
32
recipes/dvhn.recipe
Normal file
32
recipes/dvhn.recipe
Normal file
@ -0,0 +1,32 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1302341394(BasicNewsRecipe):
|
||||
title = u'DvhN'
|
||||
oldest_article = 1
|
||||
max_articles_per_feed = 200
|
||||
|
||||
__author__ = 'Reijndert'
|
||||
no_stylesheets = True
|
||||
cover_url = 'http://www.dvhn.nl/template/Dagblad_v2.0/gfx/logo_DvhN.gif'
|
||||
language = 'nl'
|
||||
country = 'NL'
|
||||
version = 1
|
||||
publisher = u'Dagblad van het Noorden'
|
||||
category = u'Nieuws'
|
||||
description = u'Nieuws uit Noord Nederland'
|
||||
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'id':'fullPicture'})
|
||||
,dict(name='div', attrs={'id':'articleText'})
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name=['object','link','iframe','base'])
|
||||
,dict(name='span',attrs={'class':'copyright'})
|
||||
]
|
||||
|
||||
feeds = [(u'Drenthe', u'http://www.dvhn.nl/nieuws/drenthe/index.jsp?service=rss'), (u'Groningen', u'http://www.dvhn.nl/nieuws/groningen/index.jsp?service=rss'), (u'Nederland', u'http://www.dvhn.nl/nieuws/nederland/index.jsp?service=rss'), (u'Wereld', u'http://www.dvhn.nl/nieuws/wereld/index.jsp?service=rss'), (u'Economie', u'http://www.dvhn.nl/nieuws/economie/index.jsp?service=rss'), (u'Sport', u'http://www.dvhn.nl/nieuws/sport/index.jsp?service=rss'), (u'Cultuur', u'http://www.dvhn.nl/nieuws/kunst/index.jsp?service=rss'), (u'24 Uur', u'http://www.dvhn.nl/nieuws/24uurdvhn/index.jsp?service=rss&selectiontype=last24hours')]
|
||||
|
||||
extra_css = '''
|
||||
body {font-family: verdana, arial, helvetica, geneva, sans-serif;}
|
||||
'''
|
@ -18,7 +18,8 @@ class Economist(BasicNewsRecipe):
|
||||
|
||||
__author__ = "Kovid Goyal"
|
||||
INDEX = 'http://www.economist.com/printedition'
|
||||
description = 'Global news and current affairs from a European perspective.'
|
||||
description = ('Global news and current affairs from a European'
|
||||
' perspective. Best downloaded on Friday mornings (GMT)')
|
||||
|
||||
oldest_article = 7.0
|
||||
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||
|
@ -11,7 +11,8 @@ class Economist(BasicNewsRecipe):
|
||||
language = 'en'
|
||||
|
||||
__author__ = "Kovid Goyal"
|
||||
description = ('Global news and current affairs from a European perspective.'
|
||||
description = ('Global news and current affairs from a European'
|
||||
' perspective. Best downloaded on Friday mornings (GMT).'
|
||||
' Much slower than the print edition based version.')
|
||||
|
||||
oldest_article = 7.0
|
||||
|
@ -11,7 +11,8 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
||||
class FinancialTimes(BasicNewsRecipe):
|
||||
title = u'Financial Times'
|
||||
__author__ = 'Darko Miletic and Sujata Raman'
|
||||
description = 'Financial world news'
|
||||
description = ('Financial world news. Available after 5AM '
|
||||
'GMT, daily.')
|
||||
oldest_article = 2
|
||||
language = 'en'
|
||||
|
||||
|
@ -88,13 +88,6 @@ categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {l
|
||||
categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}'
|
||||
|
||||
|
||||
#: Set boolean custom columns to be tristate
|
||||
# Set whether boolean custom columns are two- or three-valued.
|
||||
# Two-values for true booleans
|
||||
# three-values for yes/no/unknown
|
||||
# Set to 'yes' for three-values, 'no' for two-values
|
||||
bool_custom_columns_are_tristate = 'yes'
|
||||
|
||||
#: Specify columns to sort the booklist by on startup
|
||||
# Provide a set of columns to be sorted on when calibre starts
|
||||
# The argument is None if saved sort history is to be used
|
||||
|
BIN
resources/images/connect_share_on.png
Normal file
BIN
resources/images/connect_share_on.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.53'
|
||||
__version__ = '0.7.54'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re, importlib
|
||||
|
@ -453,12 +453,15 @@ def epub_fixers():
|
||||
# Metadata sources2 {{{
|
||||
def metadata_plugins(capabilities):
|
||||
capabilities = frozenset(capabilities)
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, Source) and \
|
||||
plugin.capabilities.intersection(capabilities) and \
|
||||
for plugin in all_metadata_plugins():
|
||||
if plugin.capabilities.intersection(capabilities) and \
|
||||
not is_disabled(plugin):
|
||||
yield plugin
|
||||
|
||||
def all_metadata_plugins():
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, Source):
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Initialize plugins {{{
|
||||
|
@ -26,9 +26,9 @@ class EDGE(USBMS):
|
||||
PRODUCT_ID = [0x0c02]
|
||||
BCD = [0x0223]
|
||||
|
||||
VENDOR_NAME = 'ANDROID'
|
||||
WINDOWS_MAIN_MEM = '__FILE-STOR_GADG'
|
||||
WINDOWS_CARD_A_MEM = '__FILE-STOR_GADG'
|
||||
VENDOR_NAME = ['ANDROID', 'LINUX']
|
||||
WINDOWS_MAIN_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET']
|
||||
WINDOWS_CARD_A_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET']
|
||||
|
||||
MAIN_MEMORY_VOLUME_LABEL = 'Edge Main Memory'
|
||||
STORAGE_CARD_VOLUME_LABEL = 'Edge Storage Card'
|
||||
|
@ -279,7 +279,7 @@ class Worker(Thread): # Get details {{{
|
||||
|
||||
class Amazon(Source):
|
||||
|
||||
name = 'Amazon Store'
|
||||
name = 'Amazon Web'
|
||||
description = _('Downloads metadata from Amazon')
|
||||
|
||||
capabilities = frozenset(['identify', 'cover'])
|
||||
@ -295,6 +295,14 @@ class Amazon(Source):
|
||||
'uk' : _('UK'),
|
||||
}
|
||||
|
||||
def get_book_url(self, identifiers): # {{{
|
||||
asin = identifiers.get('amazon', None)
|
||||
if asin is None:
|
||||
asin = identifiers.get('asin', None)
|
||||
if asin:
|
||||
return 'http://amzn.com/%s'%asin
|
||||
# }}}
|
||||
|
||||
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
|
||||
domain = self.prefs.get('domain', 'com')
|
||||
|
||||
|
@ -301,6 +301,13 @@ class Source(Plugin):
|
||||
|
||||
# Metadata API {{{
|
||||
|
||||
def get_book_url(self, identifiers):
|
||||
'''
|
||||
Return the URL for the book identified by identifiers at this source.
|
||||
If no URL is found, return None.
|
||||
'''
|
||||
return None
|
||||
|
||||
def get_cached_cover_url(self, identifiers):
|
||||
'''
|
||||
Return cached cover URL for the book identified by
|
||||
|
@ -167,6 +167,12 @@ class GoogleBooks(Source):
|
||||
|
||||
GOOGLE_COVER = 'http://books.google.com/books?id=%s&printsec=frontcover&img=1'
|
||||
|
||||
def get_book_url(self, identifiers): # {{{
|
||||
goog = identifiers.get('google', None)
|
||||
if goog is not None:
|
||||
return 'http://books.google.com/books?id=%s'%goog
|
||||
# }}}
|
||||
|
||||
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
|
||||
BASE_URL = 'http://books.google.com/books/feeds/volumes?'
|
||||
isbn = check_isbn(identifiers.get('isbn', None))
|
||||
|
@ -14,7 +14,7 @@ from threading import Thread
|
||||
from io import BytesIO
|
||||
from operator import attrgetter
|
||||
|
||||
from calibre.customize.ui import metadata_plugins
|
||||
from calibre.customize.ui import metadata_plugins, all_metadata_plugins
|
||||
from calibre.ebooks.metadata.sources.base import create_log, msprefs
|
||||
from calibre.ebooks.metadata.xisbn import xisbn
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
@ -217,6 +217,10 @@ class ISBNMerge(object):
|
||||
for r in results:
|
||||
ans.identifiers.update(r.identifiers)
|
||||
|
||||
# Cover URL
|
||||
ans.has_cached_cover_url = bool([r for r in results if
|
||||
getattr(r, 'has_cached_cover_url', False)])
|
||||
|
||||
# Merge any other fields with no special handling (random merge)
|
||||
touched_fields = set()
|
||||
for r in results:
|
||||
@ -362,6 +366,18 @@ def identify(log, abort, # {{{
|
||||
return results
|
||||
# }}}
|
||||
|
||||
def urls_from_identifiers(identifiers): # {{{
|
||||
ans = []
|
||||
for plugin in all_metadata_plugins():
|
||||
try:
|
||||
url = plugin.get_book_url(identifiers)
|
||||
if url is not None:
|
||||
ans.append((plugin.name, url))
|
||||
except:
|
||||
pass
|
||||
return ans
|
||||
# }}}
|
||||
|
||||
if __name__ == '__main__': # tests {{{
|
||||
# To run these test use: calibre-debug -e
|
||||
# src/calibre/ebooks/metadata/sources/identify.py
|
||||
|
@ -97,6 +97,10 @@ class CSSSelector(etree.XPath):
|
||||
|
||||
def __init__(self, css, namespaces=XPNSMAP):
|
||||
css = self.MIN_SPACE_RE.sub(r'\1', css)
|
||||
if isinstance(css, unicode):
|
||||
# Workaround for bug in lxml on windows/OS X that causes a massive
|
||||
# memory leak with non ASCII selectors
|
||||
css = css.encode('ascii', 'ignore').decode('ascii')
|
||||
try:
|
||||
path = css_to_xpath(css)
|
||||
except UnicodeEncodeError: # Bug in css_to_xpath
|
||||
|
@ -13,7 +13,7 @@ from functools import partial
|
||||
|
||||
from calibre.ebooks import ConversionError, DRMError
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre import isosx, iswindows, islinux, isfreebsd
|
||||
from calibre.constants import isosx, iswindows, islinux, isfreebsd
|
||||
from calibre import CurrentDir
|
||||
|
||||
PDFTOHTML = 'pdftohtml'
|
||||
@ -43,6 +43,8 @@ def pdftohtml(output_dir, pdf_path, no_images):
|
||||
# This is neccessary as pdftohtml doesn't always (linux) respect absolute paths
|
||||
pdf_path = os.path.abspath(pdf_path)
|
||||
cmd = [PDFTOHTML, '-enc', 'UTF-8', '-noframes', '-p', '-nomerge', '-nodrm', '-q', pdf_path, os.path.basename(index)]
|
||||
if isfreebsd:
|
||||
cmd.remove('-nodrm')
|
||||
if no_images:
|
||||
cmd.append('-i')
|
||||
|
||||
|
@ -13,7 +13,7 @@ from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
|
||||
|
||||
ORG_NAME = 'KovidsBrain'
|
||||
APP_UID = 'libprs500'
|
||||
from calibre.constants import islinux, iswindows, isfreebsd, isfrozen
|
||||
from calibre.constants import islinux, iswindows, isfreebsd, isfrozen, isosx
|
||||
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
||||
from calibre.utils.localization import set_qt_translator
|
||||
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
||||
@ -23,21 +23,45 @@ from calibre.utils.date import UNDEFINED_DATE
|
||||
# Setup gprefs {{{
|
||||
gprefs = JSONConfig('gui')
|
||||
|
||||
gprefs.defaults['action-layout-toolbar'] = (
|
||||
if isosx:
|
||||
gprefs.defaults['action-layout-menubar'] = (
|
||||
'Add Books', 'Edit Metadata', 'Convert Books',
|
||||
'Choose Library', 'Save To Disk', 'Preferences',
|
||||
'Help',
|
||||
)
|
||||
gprefs.defaults['action-layout-menubar-device'] = (
|
||||
'Add Books', 'Edit Metadata', 'Convert Books',
|
||||
'Location Manager', 'Send To Device',
|
||||
'Save To Disk', 'Preferences', 'Help',
|
||||
)
|
||||
gprefs.defaults['action-layout-toolbar'] = (
|
||||
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
|
||||
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
|
||||
'Connect Share', None, 'Remove Books',
|
||||
)
|
||||
gprefs.defaults['action-layout-toolbar-device'] = (
|
||||
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View',
|
||||
'Send To Device', None, None, 'Location Manager', None, None,
|
||||
'Fetch News', 'Save To Disk', 'Connect Share', None,
|
||||
'Remove Books',
|
||||
)
|
||||
else:
|
||||
gprefs.defaults['action-layout-menubar'] = ()
|
||||
gprefs.defaults['action-layout-menubar-device'] = ()
|
||||
gprefs.defaults['action-layout-toolbar'] = (
|
||||
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
|
||||
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
|
||||
'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences',
|
||||
)
|
||||
|
||||
gprefs.defaults['action-layout-toolbar-child'] = ()
|
||||
|
||||
gprefs.defaults['action-layout-toolbar-device'] = (
|
||||
gprefs.defaults['action-layout-toolbar-device'] = (
|
||||
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View',
|
||||
'Send To Device', None, None, 'Location Manager', None, None,
|
||||
'Fetch News', 'Save To Disk', 'Connect Share', None,
|
||||
'Remove Books', None, 'Help', 'Preferences',
|
||||
)
|
||||
|
||||
gprefs.defaults['action-layout-toolbar-child'] = ()
|
||||
|
||||
gprefs.defaults['action-layout-context-menu'] = (
|
||||
'Edit Metadata', 'Send To Device', 'Save To Disk',
|
||||
'Connect Share', 'Copy To Library', None,
|
||||
@ -57,6 +81,7 @@ gprefs.defaults['toolbar_text'] = 'auto'
|
||||
gprefs.defaults['font'] = None
|
||||
gprefs.defaults['tags_browser_partition_method'] = 'first letter'
|
||||
gprefs.defaults['tags_browser_collapse_at'] = 100
|
||||
gprefs.defaults['edit_metadata_single_layout'] = 'default'
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -75,7 +75,7 @@ class InterfaceAction(QObject):
|
||||
dont_remove_from = frozenset([])
|
||||
|
||||
all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu',
|
||||
'context-menu-device', 'toolbar-child'])
|
||||
'context-menu-device', 'toolbar-child', 'menubar', 'menubar-device'])
|
||||
|
||||
#: Type of action
|
||||
#: 'current' means acts on the current view
|
||||
@ -145,11 +145,10 @@ class InterfaceAction(QObject):
|
||||
ans[candidate] = zf.read(candidate)
|
||||
return ans
|
||||
|
||||
|
||||
def genesis(self):
|
||||
'''
|
||||
Setup this plugin. Only called once during initialization. self.gui is
|
||||
available. The action secified by :attr:`action_spec` is available as
|
||||
available. The action specified by :attr:`action_spec` is available as
|
||||
``self.qaction``.
|
||||
'''
|
||||
pass
|
||||
|
@ -12,7 +12,7 @@ class AddToLibraryAction(InterfaceAction):
|
||||
name = 'Add To Library'
|
||||
action_spec = (_('Add books to library'), 'add_book.png',
|
||||
_('Add books to your calibre library from the connected device'), None)
|
||||
dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
|
||||
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
|
||||
action_type = 'current'
|
||||
|
||||
def genesis(self):
|
||||
|
@ -18,6 +18,7 @@ class FetchAnnotationsAction(InterfaceAction):
|
||||
|
||||
name = 'Fetch Annotations'
|
||||
action_spec = (_('Fetch annotations (experimental)'), None, None, None)
|
||||
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
|
||||
action_type = 'current'
|
||||
|
||||
def genesis(self):
|
||||
|
@ -18,7 +18,7 @@ class GenerateCatalogAction(InterfaceAction):
|
||||
|
||||
name = 'Generate Catalog'
|
||||
action_spec = (_('Create a catalog of the books in your calibre library'), None, None, None)
|
||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||
|
||||
def generate_catalog(self):
|
||||
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import os, shutil
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QMenu, Qt, QInputDialog
|
||||
from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton
|
||||
|
||||
from calibre import isbytestring
|
||||
from calibre.constants import filesystem_encoding
|
||||
@ -80,7 +80,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
name = 'Choose Library'
|
||||
action_spec = (_('%d books'), 'lt.png',
|
||||
_('Choose calibre library to work with'), None)
|
||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||
|
||||
def genesis(self):
|
||||
self.count_changed(0)
|
||||
@ -88,6 +88,9 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
type=Qt.QueuedConnection)
|
||||
|
||||
self.stats = LibraryUsageStats()
|
||||
self.popup_type = (QToolButton.InstantPopup if len(self.stats.stats) > 1 else
|
||||
QToolButton.MenuButtonPopup)
|
||||
|
||||
self.create_action(spec=(_('Switch/create library...'), 'lt.png', None,
|
||||
None), attr='action_choose')
|
||||
self.action_choose.triggered.connect(self.choose_library,
|
||||
@ -123,6 +126,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
type=Qt.QueuedConnection)
|
||||
self.choose_menu.addAction(ac)
|
||||
|
||||
|
||||
self.rename_separator = self.choose_menu.addSeparator()
|
||||
|
||||
self.maintenance_menu = QMenu(_('Library Maintenance'))
|
||||
@ -172,6 +176,7 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
return
|
||||
db = self.gui.library_view.model().db
|
||||
locations = list(self.stats.locations(db))
|
||||
|
||||
for ac in self.switch_actions:
|
||||
ac.setVisible(False)
|
||||
self.quick_menu.clear()
|
||||
@ -205,7 +210,6 @@ class ChooseLibraryAction(InterfaceAction):
|
||||
rename_actions, delete_actions, qs_actions,
|
||||
self.action_choose)
|
||||
|
||||
|
||||
def location_selected(self, loc):
|
||||
enabled = loc == 'library'
|
||||
self.qaction.setEnabled(enabled)
|
||||
|
@ -20,7 +20,7 @@ class ConvertAction(InterfaceAction):
|
||||
|
||||
name = 'Convert Books'
|
||||
action_spec = (_('Convert books'), 'convert.png', None, _('C'))
|
||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||
action_type = 'current'
|
||||
|
||||
def genesis(self):
|
||||
|
@ -24,7 +24,7 @@ class ShareConnMenu(QMenu): # {{{
|
||||
|
||||
config_email = pyqtSignal()
|
||||
toggle_server = pyqtSignal()
|
||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QMenu.__init__(self, parent)
|
||||
@ -121,8 +121,7 @@ class SendToDeviceAction(InterfaceAction):
|
||||
|
||||
name = 'Send To Device'
|
||||
action_spec = (_('Send to device'), 'sync.png', None, _('D'))
|
||||
dont_remove_from = frozenset(['toolbar-device'])
|
||||
dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
|
||||
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
|
||||
|
||||
def genesis(self):
|
||||
self.qaction.triggered.connect(self.do_sync)
|
||||
@ -166,6 +165,10 @@ class ConnectShareAction(InterfaceAction):
|
||||
|
||||
def content_server_state_changed(self, running):
|
||||
self.share_conn_menu.server_state_changed(running)
|
||||
if running:
|
||||
self.qaction.setIcon(QIcon(I('connect_share_on.png')))
|
||||
else:
|
||||
self.qaction.setIcon(QIcon(I('connect_share.png')))
|
||||
|
||||
def toggle_content_server(self):
|
||||
if self.gui.content_server is None:
|
||||
|
@ -12,7 +12,7 @@ class EditCollectionsAction(InterfaceAction):
|
||||
name = 'Edit Collections'
|
||||
action_spec = (_('Manage collections'), None,
|
||||
_('Manage the collections on this device'), None)
|
||||
dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
|
||||
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
|
||||
action_type = 'current'
|
||||
|
||||
def genesis(self):
|
||||
|
@ -141,15 +141,18 @@ class EditMetadataAction(InterfaceAction):
|
||||
list(range(self.gui.library_view.model().rowCount(QModelIndex())))
|
||||
current_row = row_list.index(cr)
|
||||
|
||||
if test_eight_code:
|
||||
changed = self.do_edit_metadata(row_list, current_row)
|
||||
else:
|
||||
changed = self.do_edit_metadata_old(row_list, current_row)
|
||||
func = (self.do_edit_metadata if test_eight_code else
|
||||
self.do_edit_metadata_old)
|
||||
changed, rows_to_refresh = func(row_list, current_row)
|
||||
|
||||
m = self.gui.library_view.model()
|
||||
|
||||
if rows_to_refresh:
|
||||
m.refresh_rows(rows_to_refresh)
|
||||
|
||||
if changed:
|
||||
self.gui.library_view.model().refresh_ids(list(changed))
|
||||
m.refresh_ids(list(changed))
|
||||
current = self.gui.library_view.currentIndex()
|
||||
m = self.gui.library_view.model()
|
||||
if self.gui.cover_flow:
|
||||
self.gui.cover_flow.dataChanged()
|
||||
m.current_changed(current, previous)
|
||||
@ -183,6 +186,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
current_row += d.row_delta
|
||||
self.gui.library_view.set_current_row(current_row)
|
||||
self.gui.library_view.scroll_to_row(current_row)
|
||||
return changed, set()
|
||||
|
||||
def do_edit_metadata(self, row_list, current_row):
|
||||
from calibre.gui2.metadata.single import edit_metadata
|
||||
@ -190,7 +194,7 @@ class EditMetadataAction(InterfaceAction):
|
||||
changed, rows_to_refresh = edit_metadata(db, row_list, current_row,
|
||||
parent=self.gui, view_slot=self.view_format_callback,
|
||||
set_current_callback=self.set_current_callback)
|
||||
return changed
|
||||
return changed, rows_to_refresh
|
||||
|
||||
def set_current_callback(self, id_):
|
||||
db = self.gui.library_view.model().db
|
||||
|
@ -11,7 +11,7 @@ class NextMatchAction(InterfaceAction):
|
||||
name = 'Move to next highlighted book'
|
||||
action_spec = (_('Move to next match'), 'arrow-down.png',
|
||||
_('Move to next highlighted match'), [_('N'), _('F3')])
|
||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||
action_type = 'current'
|
||||
|
||||
def genesis(self):
|
||||
|
@ -13,7 +13,7 @@ class OpenFolderAction(InterfaceAction):
|
||||
name = 'Open Folder'
|
||||
action_spec = (_('Open containing folder'), 'document_open.png', None,
|
||||
_('O'))
|
||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||
action_type = 'current'
|
||||
|
||||
def genesis(self):
|
||||
|
@ -16,7 +16,6 @@ class PreferencesAction(InterfaceAction):
|
||||
|
||||
name = 'Preferences'
|
||||
action_spec = (_('Preferences'), 'config.png', None, _('Ctrl+P'))
|
||||
dont_remove_from = frozenset(['toolbar'])
|
||||
|
||||
def genesis(self):
|
||||
pm = QMenu()
|
||||
|
@ -15,7 +15,7 @@ class ShowBookDetailsAction(InterfaceAction):
|
||||
name = 'Show Book Details'
|
||||
action_spec = (_('Show book details'), 'dialog_information.png', None,
|
||||
_('I'))
|
||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||
action_type = 'current'
|
||||
|
||||
def genesis(self):
|
||||
|
@ -15,7 +15,7 @@ class TweakEpubAction(InterfaceAction):
|
||||
action_spec = (_('Tweak ePub'), 'trim.png',
|
||||
_('Make small changes to ePub format books'),
|
||||
_('T'))
|
||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||
action_type = 'current'
|
||||
|
||||
def genesis(self):
|
||||
|
@ -62,7 +62,7 @@ class Bool(Base):
|
||||
w = self.widgets[1]
|
||||
items = [_('Yes'), _('No'), _('Undefined')]
|
||||
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||
if not self.db.prefs.get('bools_are_tristate'):
|
||||
items = items[:-1]
|
||||
icons = icons[:-1]
|
||||
for icon, text in zip(icons, items):
|
||||
@ -70,7 +70,7 @@ class Bool(Base):
|
||||
|
||||
def setter(self, val):
|
||||
val = {None: 2, False: 1, True: 0}[val]
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val == 2:
|
||||
if not self.db.prefs.get('bools_are_tristate') and val == 2:
|
||||
val = 1
|
||||
self.widgets[1].setCurrentIndex(val)
|
||||
|
||||
@ -549,7 +549,7 @@ class BulkBool(BulkBase, Bool):
|
||||
value = None
|
||||
for book_id in book_ids:
|
||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||
if not self.db.prefs.get('bools_are_tristate') and val is None:
|
||||
val = False
|
||||
if value is not None and value != val:
|
||||
return None
|
||||
@ -559,7 +559,7 @@ class BulkBool(BulkBase, Bool):
|
||||
def setup_ui(self, parent):
|
||||
self.make_widgets(parent, QComboBox)
|
||||
items = [_('Yes'), _('No')]
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||
if not self.db.prefs.get('bools_are_tristate'):
|
||||
items.append('')
|
||||
else:
|
||||
items.append(_('Undefined'))
|
||||
@ -571,7 +571,7 @@ class BulkBool(BulkBase, Bool):
|
||||
|
||||
def getter(self):
|
||||
val = self.main_widget.currentIndex()
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||
if not self.db.prefs.get('bools_are_tristate'):
|
||||
return {2: False, 1: False, 0: True}[val]
|
||||
else:
|
||||
return {2: None, 1: False, 0: True}[val]
|
||||
@ -586,13 +586,13 @@ class BulkBool(BulkBase, Bool):
|
||||
return
|
||||
val = self.gui_val
|
||||
val = self.normalize_ui_val(val)
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||
if not self.db.prefs.get('bools_are_tristate') and val is None:
|
||||
val = False
|
||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||
|
||||
def a_c_checkbox_changed(self):
|
||||
if not self.ignore_change_signals:
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and \
|
||||
if not self.db.prefs.get('bools_are_tristate') and \
|
||||
self.main_widget.currentIndex() == 2:
|
||||
self.a_c_checkbox.setChecked(False)
|
||||
else:
|
||||
|
@ -7,12 +7,12 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \
|
||||
pyqtSignal, QToolButton, QMenu, \
|
||||
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup
|
||||
from PyQt4.Qt import (QIcon, Qt, QWidget, QToolBar, QSize,
|
||||
pyqtSignal, QToolButton, QMenu, QMenuBar, QAction,
|
||||
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
|
||||
|
||||
|
||||
from calibre.constants import __appname__
|
||||
from calibre.constants import __appname__, isosx
|
||||
from calibre.gui2.search_box import SearchBox2, SavedSearchBox
|
||||
from calibre.gui2.throbber import ThrobbingButton
|
||||
from calibre.gui2 import gprefs
|
||||
@ -238,18 +238,117 @@ class Spacer(QWidget): # {{{
|
||||
self.l.addStretch(10)
|
||||
# }}}
|
||||
|
||||
class ToolBar(QToolBar): # {{{
|
||||
class MenuAction(QAction): # {{{
|
||||
|
||||
def __init__(self, donate, location_manager, child_bar, parent):
|
||||
QToolBar.__init__(self, parent)
|
||||
def __init__(self, clone, parent):
|
||||
QAction.__init__(self, clone.text(), parent)
|
||||
self.clone = clone
|
||||
clone.changed.connect(self.clone_changed)
|
||||
|
||||
def clone_changed(self):
|
||||
self.setText(self.clone.text())
|
||||
# }}}
|
||||
|
||||
class MenuBar(QMenuBar): # {{{
|
||||
|
||||
def __init__(self, location_manager, parent):
|
||||
QMenuBar.__init__(self, parent)
|
||||
self.gui = parent
|
||||
self.child_bar = child_bar
|
||||
self.setNativeMenuBar(True)
|
||||
|
||||
self.location_manager = location_manager
|
||||
self.location_manager.locations_changed.connect(self.build_bar)
|
||||
self.added_actions = []
|
||||
|
||||
self.donate_action = QAction(_('Donate'), self)
|
||||
self.donate_menu = QMenu()
|
||||
self.donate_menu.addAction(self.gui.donate_action)
|
||||
self.donate_action.setMenu(self.donate_menu)
|
||||
self.build_bar()
|
||||
|
||||
def build_bar(self, changed_action=None):
|
||||
showing_device = self.location_manager.has_device
|
||||
actions = '-device' if showing_device else ''
|
||||
actions = gprefs['action-layout-menubar'+actions]
|
||||
|
||||
show_main = len(actions) > 0
|
||||
self.setVisible(show_main)
|
||||
|
||||
for ac in self.added_actions:
|
||||
m = ac.menu()
|
||||
if m is not None:
|
||||
m.setVisible(False)
|
||||
|
||||
self.clear()
|
||||
self.added_actions = []
|
||||
self.action_map = {}
|
||||
|
||||
for what in actions:
|
||||
if what is None:
|
||||
continue
|
||||
elif what == 'Location Manager':
|
||||
for ac in self.location_manager.available_actions:
|
||||
ac = self.build_menu(ac)
|
||||
self.addAction(ac)
|
||||
self.added_actions.append(ac)
|
||||
elif what == 'Donate':
|
||||
self.addAction(self.donate_action)
|
||||
elif what in self.gui.iactions:
|
||||
action = self.gui.iactions[what]
|
||||
ac = self.build_menu(action.qaction)
|
||||
self.addAction(ac)
|
||||
self.added_actions.append(ac)
|
||||
|
||||
def build_menu(self, action):
|
||||
m = action.menu()
|
||||
ac = MenuAction(action, self)
|
||||
if m is None:
|
||||
m = QMenu()
|
||||
m.addAction(action)
|
||||
ac.setMenu(m)
|
||||
return ac
|
||||
|
||||
# }}}
|
||||
|
||||
class BaseToolBar(QToolBar): # {{{
|
||||
|
||||
def __init__(self, parent):
|
||||
QToolBar.__init__(self, parent)
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
self.setMovable(False)
|
||||
self.setFloatable(False)
|
||||
self.setOrientation(Qt.Horizontal)
|
||||
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
|
||||
self.setStyleSheet('QToolButton:checked { font-weight: bold }')
|
||||
self.preferred_width = self.sizeHint().width()
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
QToolBar.resizeEvent(self, ev)
|
||||
style = self.get_text_style()
|
||||
self.setToolButtonStyle(style)
|
||||
|
||||
def get_text_style(self):
|
||||
style = Qt.ToolButtonTextUnderIcon
|
||||
s = gprefs['toolbar_icon_size']
|
||||
if s != 'off':
|
||||
p = gprefs['toolbar_text']
|
||||
if p == 'never':
|
||||
style = Qt.ToolButtonIconOnly
|
||||
elif p == 'auto' and self.preferred_width > self.width()+35:
|
||||
style = Qt.ToolButtonIconOnly
|
||||
return style
|
||||
|
||||
def contextMenuEvent(self, *args):
|
||||
pass
|
||||
|
||||
# }}}
|
||||
|
||||
class ToolBar(BaseToolBar): # {{{
|
||||
|
||||
def __init__(self, donate, location_manager, child_bar, parent):
|
||||
BaseToolBar.__init__(self, parent)
|
||||
self.gui = parent
|
||||
self.child_bar = child_bar
|
||||
self.donate_button = donate
|
||||
self.apply_settings()
|
||||
|
||||
@ -259,24 +358,20 @@ class ToolBar(QToolBar): # {{{
|
||||
donate.setCursor(Qt.PointingHandCursor)
|
||||
self.added_actions = []
|
||||
self.build_bar()
|
||||
self.preferred_width = self.sizeHint().width()
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
def apply_settings(self):
|
||||
sz = gprefs['toolbar_icon_size']
|
||||
sz = {'small':24, 'medium':48, 'large':64}[sz]
|
||||
sz = {'off':0, 'small':24, 'medium':48, 'large':64}[sz]
|
||||
self.setIconSize(QSize(sz, sz))
|
||||
self.child_bar.setIconSize(QSize(sz, sz))
|
||||
style = Qt.ToolButtonTextUnderIcon
|
||||
if gprefs['toolbar_text'] == 'never':
|
||||
if sz > 0 and gprefs['toolbar_text'] == 'never':
|
||||
style = Qt.ToolButtonIconOnly
|
||||
self.setToolButtonStyle(style)
|
||||
self.child_bar.setToolButtonStyle(style)
|
||||
self.donate_button.set_normal_icon_size(sz, sz)
|
||||
|
||||
def contextMenuEvent(self, *args):
|
||||
pass
|
||||
|
||||
def build_bar(self):
|
||||
self.showing_donate = False
|
||||
showing_device = self.location_manager.has_device
|
||||
@ -284,6 +379,8 @@ class ToolBar(QToolBar): # {{{
|
||||
mactions = gprefs['action-layout-toolbar'+mactions]
|
||||
cactions = gprefs['action-layout-toolbar-child']
|
||||
|
||||
show_main = len(mactions) > 0
|
||||
self.setVisible(show_main)
|
||||
show_child = len(cactions) > 0
|
||||
self.child_bar.setVisible(show_child)
|
||||
|
||||
@ -309,6 +406,8 @@ class ToolBar(QToolBar): # {{{
|
||||
self.d_widget = QWidget()
|
||||
self.d_widget.setLayout(QVBoxLayout())
|
||||
self.d_widget.layout().addWidget(self.donate_button)
|
||||
if isosx:
|
||||
self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }')
|
||||
bar.addWidget(self.d_widget)
|
||||
self.showing_donate = True
|
||||
elif what in self.gui.iactions:
|
||||
@ -316,6 +415,8 @@ class ToolBar(QToolBar): # {{{
|
||||
bar.addAction(action.qaction)
|
||||
self.added_actions.append(action.qaction)
|
||||
self.setup_tool_button(bar, action.qaction, action.popup_type)
|
||||
self.preferred_width = self.sizeHint().width()
|
||||
self.child_bar.preferred_width = self.child_bar.sizeHint().width()
|
||||
|
||||
def setup_tool_button(self, bar, ac, menu_mode=None):
|
||||
ch = bar.widgetForAction(ac)
|
||||
@ -325,19 +426,7 @@ class ToolBar(QToolBar): # {{{
|
||||
ch.setAutoRaise(True)
|
||||
if ac.menu() is not None and menu_mode is not None:
|
||||
ch.setPopupMode(menu_mode)
|
||||
|
||||
def resizeEvent(self, ev):
|
||||
QToolBar.resizeEvent(self, ev)
|
||||
style = Qt.ToolButtonTextUnderIcon
|
||||
p = gprefs['toolbar_text']
|
||||
if p == 'never':
|
||||
style = Qt.ToolButtonIconOnly
|
||||
|
||||
if p == 'auto' and self.preferred_width > self.width()+35 and \
|
||||
not gprefs['action-layout-toolbar-child']:
|
||||
style = Qt.ToolButtonIconOnly
|
||||
|
||||
self.setToolButtonStyle(style)
|
||||
return ch
|
||||
|
||||
def database_changed(self, db):
|
||||
pass
|
||||
@ -416,11 +505,14 @@ class MainWindowMixin(object): # {{{
|
||||
self.iactions['Fetch News'].init_scheduler(db)
|
||||
|
||||
self.search_bar = SearchBar(self)
|
||||
self.child_bar = QToolBar(self)
|
||||
self.child_bar = BaseToolBar(self)
|
||||
self.tool_bar = ToolBar(self.donate_button,
|
||||
self.location_manager, self.child_bar, self)
|
||||
self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
|
||||
self.addToolBar(Qt.BottomToolBarArea, self.child_bar)
|
||||
self.menu_bar = MenuBar(self.location_manager, self)
|
||||
self.setMenuBar(self.menu_bar)
|
||||
self.setUnifiedTitleAndToolBarOnMac(True)
|
||||
|
||||
l = self.centralwidget.layout()
|
||||
l.addWidget(self.search_bar)
|
||||
|
@ -353,7 +353,7 @@ class CcBoolDelegate(QStyledItemDelegate): # {{{
|
||||
editor = DelegateCB(parent)
|
||||
items = [_('Y'), _('N'), ' ']
|
||||
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||
if not index.model().db.prefs.get('bools_are_tristate'):
|
||||
items = items[:-1]
|
||||
icons = icons[:-1]
|
||||
for icon, text in zip(icons, items):
|
||||
@ -367,7 +367,7 @@ class CcBoolDelegate(QStyledItemDelegate): # {{{
|
||||
def setEditorData(self, editor, index):
|
||||
m = index.model()
|
||||
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||
if not m.db.prefs.get('bools_are_tristate'):
|
||||
val = 1 if not val else 0
|
||||
else:
|
||||
val = 2 if val is None else 1 if not val else 0
|
||||
|
@ -700,7 +700,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.dc_decorator[col] = functools.partial(
|
||||
bool_type_decorator, idx=idx,
|
||||
bool_cols_are_tristate=
|
||||
tweaks['bool_custom_columns_are_tristate'] != 'no')
|
||||
self.db.prefs.get('bools_are_tristate'))
|
||||
elif datatype in ('int', 'float'):
|
||||
self.dc[col] = functools.partial(number_type, idx=idx)
|
||||
elif datatype == 'datetime':
|
||||
@ -710,7 +710,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.dc_decorator[col] = functools.partial(
|
||||
bool_type_decorator, idx=idx,
|
||||
bool_cols_are_tristate=
|
||||
tweaks['bool_custom_columns_are_tristate'] != 'no')
|
||||
self.db.prefs.get('bools_are_tristate'))
|
||||
elif datatype == 'rating':
|
||||
self.dc[col] = functools.partial(rating_type, idx=idx)
|
||||
elif datatype == 'series':
|
||||
|
@ -521,7 +521,7 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
|
||||
class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
|
||||
|
||||
cc_two_column = False
|
||||
one_line_comments_toolbar = True
|
||||
@ -654,10 +654,14 @@ class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1}
|
||||
|
||||
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
|
||||
set_current_callback=None):
|
||||
d = MetadataSingleDialog(db, parent)
|
||||
cls = gprefs.get('edit_metadata_single_layout', '')
|
||||
if cls not in editors:
|
||||
cls = 'default'
|
||||
d = editors[cls](db, parent)
|
||||
d.start(row_list, current_row, view_slot=view_slot,
|
||||
set_current_callback=set_current_callback)
|
||||
return d.changed, d.rows_to_refresh
|
||||
|
@ -7,26 +7,30 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
DEBUG_DIALOG = False
|
||||
|
||||
# Imports {{{
|
||||
from threading import Thread, Event
|
||||
from operator import attrgetter
|
||||
from Queue import Queue, Empty
|
||||
|
||||
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
|
||||
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
|
||||
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette)
|
||||
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette,
|
||||
QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView,
|
||||
QPixmap, QAbstractListModel, QColor, QRect, QTextBrowser)
|
||||
from PyQt4.QtWebKit import QWebView
|
||||
|
||||
from calibre.customize.ui import metadata_plugins
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre.utils.logging import ThreadSafeLog, UnicodeHTMLStream
|
||||
from calibre.ebooks.metadata.sources.identify import identify
|
||||
|
||||
class Log(ThreadSafeLog): # {{{
|
||||
|
||||
def __init__(self):
|
||||
ThreadSafeLog.__init__(self, level=self.DEBUG)
|
||||
self.outputs = [UnicodeHTMLStream()]
|
||||
|
||||
def clear(self):
|
||||
self.outputs[0].clear()
|
||||
from calibre.utils.logging import GUILog as Log
|
||||
from calibre.ebooks.metadata.sources.identify import (identify,
|
||||
urls_from_identifiers)
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
from calibre.gui2 import error_dialog, NONE
|
||||
from calibre.utils.date import utcnow, fromordinal, format_date
|
||||
from calibre.library.comments import comments_to_html
|
||||
from calibre import force_unicode
|
||||
# }}}
|
||||
|
||||
class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||
@ -40,7 +44,11 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||
return doc
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
ans = self.to_doc(index).size().toSize()
|
||||
doc = self.to_doc(index)
|
||||
ans = doc.size().toSize()
|
||||
if ans.width() > 250:
|
||||
doc.setTextWidth(250)
|
||||
ans = doc.size().toSize()
|
||||
ans.setHeight(ans.height()+10)
|
||||
return ans
|
||||
|
||||
@ -56,24 +64,227 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||
painter.restore()
|
||||
# }}}
|
||||
|
||||
class ResultsView(QTableView):
|
||||
class CoverDelegate(QStyledItemDelegate): # {{{
|
||||
|
||||
needs_redraw = pyqtSignal()
|
||||
|
||||
def __init__(self, parent):
|
||||
QStyledItemDelegate.__init__(self, parent)
|
||||
|
||||
self.angle = 0
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.frame_changed)
|
||||
self.color = parent.palette().color(QPalette.WindowText)
|
||||
self.spinner_width = 64
|
||||
|
||||
def frame_changed(self, *args):
|
||||
self.angle = (self.angle+30)%360
|
||||
self.needs_redraw.emit()
|
||||
|
||||
def start_animation(self):
|
||||
self.angle = 0
|
||||
self.timer.start(200)
|
||||
|
||||
def stop_animation(self):
|
||||
self.timer.stop()
|
||||
|
||||
def draw_spinner(self, painter, rect):
|
||||
width = rect.width()
|
||||
|
||||
outer_radius = (width-1)*0.5
|
||||
inner_radius = (width-1)*0.5*0.38
|
||||
|
||||
capsule_height = outer_radius - inner_radius
|
||||
capsule_width = int(capsule_height * (0.23 if width > 32 else 0.35))
|
||||
capsule_radius = capsule_width//2
|
||||
|
||||
painter.save()
|
||||
painter.setRenderHint(painter.Antialiasing)
|
||||
|
||||
for i in xrange(12):
|
||||
color = QColor(self.color)
|
||||
color.setAlphaF(1.0 - (i/12.0))
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.setBrush(color)
|
||||
painter.save()
|
||||
painter.translate(rect.center())
|
||||
painter.rotate(self.angle - i*30.0)
|
||||
painter.drawRoundedRect(-capsule_width*0.5,
|
||||
-(inner_radius+capsule_height), capsule_width,
|
||||
capsule_height, capsule_radius, capsule_radius)
|
||||
painter.restore()
|
||||
painter.restore()
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
QStyledItemDelegate.paint(self, painter, option, index)
|
||||
if self.timer.isActive() and index.data(Qt.UserRole).toBool():
|
||||
rect = QRect(0, 0, self.spinner_width, self.spinner_width)
|
||||
rect.moveCenter(option.rect.center())
|
||||
self.draw_spinner(painter, rect)
|
||||
# }}}
|
||||
|
||||
class ResultsModel(QAbstractTableModel): # {{{
|
||||
|
||||
COLUMNS = (
|
||||
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
|
||||
)
|
||||
HTML_COLS = (1, 2)
|
||||
ICON_COLS = (3, 4)
|
||||
|
||||
def __init__(self, results, parent=None):
|
||||
QAbstractTableModel.__init__(self, parent)
|
||||
self.results = results
|
||||
self.yes_icon = QVariant(QIcon(I('ok.png')))
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return len(self.results)
|
||||
|
||||
def columnCount(self, parent=None):
|
||||
return len(self.COLUMNS)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
try:
|
||||
return QVariant(self.COLUMNS[section])
|
||||
except:
|
||||
return NONE
|
||||
return NONE
|
||||
|
||||
def data_as_text(self, book, col):
|
||||
if col == 0:
|
||||
return unicode(book.gui_rank+1)
|
||||
if col == 1:
|
||||
t = book.title if book.title else _('Unknown')
|
||||
a = authors_to_string(book.authors) if book.authors else ''
|
||||
return '<b>%s</b><br><i>%s</i>' % (t, a)
|
||||
if col == 2:
|
||||
d = format_date(book.pubdate, 'yyyy') if book.pubdate else _('Unknown')
|
||||
p = book.publisher if book.publisher else ''
|
||||
return '<b>%s</b><br><i>%s</i>' % (d, p)
|
||||
|
||||
|
||||
def data(self, index, role):
|
||||
row, col = index.row(), index.column()
|
||||
try:
|
||||
book = self.results[row]
|
||||
except:
|
||||
return NONE
|
||||
if role == Qt.DisplayRole and col not in self.ICON_COLS:
|
||||
res = self.data_as_text(book, col)
|
||||
if res:
|
||||
return QVariant(res)
|
||||
return NONE
|
||||
elif role == Qt.DecorationRole and col in self.ICON_COLS:
|
||||
if col == 3 and getattr(book, 'has_cached_cover_url', False):
|
||||
return self.yes_icon
|
||||
if col == 4 and book.comments:
|
||||
return self.yes_icon
|
||||
elif role == Qt.UserRole:
|
||||
return book
|
||||
return NONE
|
||||
|
||||
def sort(self, col, order=Qt.AscendingOrder):
|
||||
key = lambda x: x
|
||||
if col == 0:
|
||||
key = attrgetter('gui_rank')
|
||||
elif col == 1:
|
||||
key = attrgetter('title')
|
||||
elif col == 2:
|
||||
key = attrgetter('authors')
|
||||
elif col == 3:
|
||||
key = attrgetter('has_cached_cover_url')
|
||||
elif key == 4:
|
||||
key = lambda x: bool(x.comments)
|
||||
|
||||
self.results.sort(key=key, reverse=order==Qt.AscendingOrder)
|
||||
self.reset()
|
||||
|
||||
# }}}
|
||||
|
||||
class ResultsView(QTableView): # {{{
|
||||
|
||||
show_details_signal = pyqtSignal(object)
|
||||
book_selected = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QTableView.__init__(self, parent)
|
||||
self.rt_delegate = RichTextDelegate(self)
|
||||
self.setSelectionMode(self.SingleSelection)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSelectionBehavior(self.SelectRows)
|
||||
self.setIconSize(QSize(24, 24))
|
||||
self.clicked.connect(self.show_details)
|
||||
self.doubleClicked.connect(self.select_index)
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
def show_results(self, results):
|
||||
self._model = ResultsModel(results, self)
|
||||
self.setModel(self._model)
|
||||
for i in self._model.HTML_COLS:
|
||||
self.setItemDelegateForColumn(i, self.rt_delegate)
|
||||
self.resizeRowsToContents()
|
||||
self.resizeColumnsToContents()
|
||||
self.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
def currentChanged(self, current, previous):
|
||||
ret = QTableView.currentChanged(self, current, previous)
|
||||
self.show_details(current)
|
||||
return ret
|
||||
|
||||
def show_details(self, index):
|
||||
book = self.model().data(index, Qt.UserRole)
|
||||
parts = [
|
||||
'<center>',
|
||||
'<h2>%s</h2>'%book.title,
|
||||
'<div><i>%s</i></div>'%authors_to_string(book.authors),
|
||||
]
|
||||
if not book.is_null('rating'):
|
||||
parts.append('<div>%s</div>'%('\u2605'*int(book.rating)))
|
||||
parts.append('</center>')
|
||||
if book.identifiers:
|
||||
urls = urls_from_identifiers(book.identifiers)
|
||||
ids = ['<a href="%s">%s</a>'%(url, name) for name, url in urls]
|
||||
if ids:
|
||||
parts.append('<div><b>%s:</b> %s</div><br>'%(_('See at'), ', '.join(ids)))
|
||||
if book.tags:
|
||||
parts.append('<div>%s</div><div>\u00a0</div>'%', '.join(book.tags))
|
||||
if book.comments:
|
||||
parts.append(comments_to_html(book.comments))
|
||||
|
||||
self.show_details_signal.emit(''.join(parts))
|
||||
|
||||
def select_index(self, index):
|
||||
if not index.isValid():
|
||||
index = self.model().index(0, 0)
|
||||
book = self.model().data(index, Qt.UserRole)
|
||||
self.book_selected.emit(book)
|
||||
|
||||
def get_result(self):
|
||||
self.select_index(self.currentIndex())
|
||||
|
||||
# }}}
|
||||
|
||||
class Comments(QWebView): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QWebView.__init__(self, parent)
|
||||
self.setAcceptDrops(False)
|
||||
self.setMaximumWidth(270)
|
||||
self.setMinimumWidth(270)
|
||||
self.setMaximumWidth(300)
|
||||
self.setMinimumWidth(300)
|
||||
|
||||
palette = self.palette()
|
||||
palette.setBrush(QPalette.Base, Qt.transparent)
|
||||
self.page().setPalette(palette)
|
||||
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
|
||||
|
||||
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
|
||||
self.linkClicked.connect(self.link_clicked)
|
||||
|
||||
def link_clicked(self, url):
|
||||
from calibre.gui2 import open_url
|
||||
if unicode(url.toString()).startswith('http://'):
|
||||
open_url(url)
|
||||
|
||||
def turnoff_scrollbar(self, *args):
|
||||
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
||||
|
||||
@ -109,7 +320,7 @@ class Comments(QWebView): # {{{
|
||||
self.setHtml(templ%html)
|
||||
# }}}
|
||||
|
||||
class IdentifyWorker(Thread):
|
||||
class IdentifyWorker(Thread): # {{{
|
||||
|
||||
def __init__(self, log, abort, title, authors, identifiers):
|
||||
Thread.__init__(self)
|
||||
@ -122,17 +333,42 @@ class IdentifyWorker(Thread):
|
||||
self.results = []
|
||||
self.error = None
|
||||
|
||||
def sample_results(self):
|
||||
m1 = Metadata('The Great Gatsby', ['Francis Scott Fitzgerald'])
|
||||
m2 = Metadata('The Great Gatsby', ['F. Scott Fitzgerald'])
|
||||
m1.has_cached_cover_url = True
|
||||
m2.has_cached_cover_url = False
|
||||
m1.comments = 'Some comments '*10
|
||||
m1.tags = ['tag%d'%i for i in range(20)]
|
||||
m1.rating = 4.4
|
||||
m1.language = 'en'
|
||||
m2.language = 'fr'
|
||||
m1.pubdate = utcnow()
|
||||
m2.pubdate = fromordinal(1000000)
|
||||
m1.publisher = 'Publisher 1'
|
||||
m2.publisher = 'Publisher 2'
|
||||
|
||||
return [m1, m2]
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if DEBUG_DIALOG:
|
||||
self.results = self.sample_results()
|
||||
else:
|
||||
self.results = identify(self.log, self.abort, title=self.title,
|
||||
authors=self.authors, identifiers=self.identifiers)
|
||||
for i, result in enumerate(self.results):
|
||||
result.gui_rank = i
|
||||
except:
|
||||
import traceback
|
||||
self.error = traceback.format_exc()
|
||||
self.error = force_unicode(traceback.format_exc())
|
||||
# }}}
|
||||
|
||||
class IdentifyWidget(QWidget):
|
||||
class IdentifyWidget(QWidget): # {{{
|
||||
|
||||
rejected = pyqtSignal()
|
||||
results_found = pyqtSignal()
|
||||
book_selected = pyqtSignal(object)
|
||||
|
||||
def __init__(self, log, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
@ -150,11 +386,15 @@ class IdentifyWidget(QWidget):
|
||||
l.addWidget(self.top, 0, 0)
|
||||
|
||||
self.results_view = ResultsView(self)
|
||||
self.results_view.book_selected.connect(self.book_selected.emit)
|
||||
self.get_result = self.results_view.get_result
|
||||
l.addWidget(self.results_view, 1, 0)
|
||||
|
||||
self.comments_view = Comments(self)
|
||||
l.addWidget(self.comments_view, 1, 1)
|
||||
|
||||
self.results_view.show_details_signal.connect(self.comments_view.show_data)
|
||||
|
||||
self.query = QLabel('download starting...')
|
||||
f = self.query.font()
|
||||
f.setPointSize(f.pointSize()-2)
|
||||
@ -162,7 +402,7 @@ class IdentifyWidget(QWidget):
|
||||
self.query.setWordWrap(True)
|
||||
l.addWidget(self.query, 2, 0, 1, 2)
|
||||
|
||||
self.comments_view.show_data('<h2>'+_('Downloading')+
|
||||
self.comments_view.show_data('<h2>'+_('Please wait')+
|
||||
'<br><span id="dots">.</span></h2>'+
|
||||
'''
|
||||
<script type="text/javascript">
|
||||
@ -197,13 +437,352 @@ class IdentifyWidget(QWidget):
|
||||
self.worker = IdentifyWorker(self.log, self.abort, title,
|
||||
authors, identifiers)
|
||||
|
||||
# self.worker.start()
|
||||
self.worker.start()
|
||||
|
||||
class FullFetch(QDialog): # {{{
|
||||
QTimer.singleShot(50, self.update)
|
||||
|
||||
def update(self):
|
||||
if self.worker.is_alive():
|
||||
QTimer.singleShot(50, self.update)
|
||||
else:
|
||||
self.process_results()
|
||||
|
||||
def process_results(self):
|
||||
if self.worker.error is not None:
|
||||
error_dialog(self, _('Download failed'),
|
||||
_('Failed to download metadata. Click '
|
||||
'Show Details to see details'),
|
||||
show=True, det_msg=self.worker.error)
|
||||
self.rejected.emit()
|
||||
return
|
||||
|
||||
if not self.worker.results:
|
||||
log = ''.join(self.log.plain_text)
|
||||
error_dialog(self, _('No matches found'), '<p>' +
|
||||
_('Failed to find any books that '
|
||||
'match your search. Try making the search <b>less '
|
||||
'specific</b>. For example, use only the author\'s '
|
||||
'last name and a single distinctive word from '
|
||||
'the title.<p>To see the full log, click Show Details.'),
|
||||
show=True, det_msg=log)
|
||||
self.rejected.emit()
|
||||
return
|
||||
|
||||
self.results_view.show_results(self.worker.results)
|
||||
|
||||
self.comments_view.show_data('''
|
||||
<div style="margin-bottom:2ex">Found <b>%d</b> results</div>
|
||||
<div>To see <b>details</b>, click on any result</div>''' %
|
||||
len(self.worker.results))
|
||||
|
||||
self.results_found.emit()
|
||||
|
||||
|
||||
def cancel(self):
|
||||
self.abort.set()
|
||||
# }}}
|
||||
|
||||
class CoverWorker(Thread): # {{{
|
||||
|
||||
def __init__(self, log, abort, title, authors, identifiers):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
|
||||
self.log, self.abort = log, abort
|
||||
self.title, self.authors, self.identifiers = (title, authors,
|
||||
identifiers)
|
||||
|
||||
self.rq = Queue()
|
||||
self.error = None
|
||||
|
||||
def fake_run(self):
|
||||
images = ['donate.png', 'config.png', 'column.png', 'eject.png', ]
|
||||
import time
|
||||
time.sleep(2)
|
||||
for pl, im in zip(metadata_plugins(['cover']), images):
|
||||
self.rq.put((pl, 1, 1, 'png', I(im, data=True)))
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if DEBUG_DIALOG:
|
||||
self.fake_run()
|
||||
else:
|
||||
from calibre.ebooks.metadata.sources.covers import run_download
|
||||
run_download(self.log, self.rq, self.abort, title=self.title,
|
||||
authors=self.authors, identifiers=self.identifiers)
|
||||
except:
|
||||
import traceback
|
||||
self.error = force_unicode(traceback.format_exc())
|
||||
# }}}
|
||||
|
||||
class CoversModel(QAbstractListModel): # {{{
|
||||
|
||||
def __init__(self, current_cover, parent=None):
|
||||
QAbstractListModel.__init__(self, parent)
|
||||
|
||||
if current_cover is None:
|
||||
current_cover = QPixmap(I('default_cover.png'))
|
||||
|
||||
self.blank = QPixmap(I('blank.png')).scaled(150, 200)
|
||||
|
||||
self.covers = [self.get_item(_('Current cover'), current_cover)]
|
||||
self.plugin_map = {}
|
||||
for i, plugin in enumerate(metadata_plugins(['cover'])):
|
||||
self.covers.append((plugin.name+'\n'+_('Searching...'),
|
||||
QVariant(self.blank), None, True))
|
||||
self.plugin_map[plugin] = i+1
|
||||
|
||||
def get_item(self, src, pmap, waiting=False):
|
||||
sz = '%dx%d'%(pmap.width(), pmap.height())
|
||||
text = QVariant(src + '\n' + sz)
|
||||
scaled = pmap.scaled(150, 200, Qt.IgnoreAspectRatio,
|
||||
Qt.SmoothTransformation)
|
||||
return (text, QVariant(scaled), pmap, waiting)
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return len(self.covers)
|
||||
|
||||
def data(self, index, role):
|
||||
try:
|
||||
text, pmap, cover, waiting = self.covers[index.row()]
|
||||
except:
|
||||
return NONE
|
||||
if role == Qt.DecorationRole:
|
||||
return pmap
|
||||
if role == Qt.DisplayRole:
|
||||
return text
|
||||
if role == Qt.UserRole:
|
||||
return waiting
|
||||
return NONE
|
||||
|
||||
def plugin_for_index(self, index):
|
||||
row = index.row() if hasattr(index, 'row') else index
|
||||
for k, v in self.plugin_map.iteritems():
|
||||
if v == row:
|
||||
return k
|
||||
|
||||
def clear_failed(self):
|
||||
good = []
|
||||
pmap = {}
|
||||
for i, x in enumerate(self.covers):
|
||||
if not x[-1]:
|
||||
good.append(x)
|
||||
if i > 0:
|
||||
plugin = self.plugin_for_index(i)
|
||||
pmap[plugin] = len(good) - 1
|
||||
good = [x for x in self.covers if not x[-1]]
|
||||
self.covers = good
|
||||
self.plugin_map = pmap
|
||||
self.reset()
|
||||
|
||||
def index_for_plugin(self, plugin):
|
||||
idx = self.plugin_map.get(plugin, 0)
|
||||
return self.index(idx)
|
||||
|
||||
def update_result(self, plugin, width, height, data):
|
||||
try:
|
||||
idx = self.plugin_map[plugin]
|
||||
except:
|
||||
return
|
||||
pmap = QPixmap()
|
||||
pmap.loadFromData(data)
|
||||
if pmap.isNull():
|
||||
return
|
||||
self.covers[idx] = self.get_item(plugin.name, pmap, waiting=False)
|
||||
self.dataChanged.emit(self.index(idx), self.index(idx))
|
||||
|
||||
def cover_pixmap(self, index):
|
||||
row = index.row()
|
||||
if row > 0 and row < len(self.covers):
|
||||
pmap = self.covers[row][2]
|
||||
if pmap is not None and not pmap.isNull():
|
||||
return pmap
|
||||
|
||||
# }}}
|
||||
|
||||
class CoversView(QListView): # {{{
|
||||
|
||||
chosen = pyqtSignal()
|
||||
|
||||
def __init__(self, current_cover, parent=None):
|
||||
QListView.__init__(self, parent)
|
||||
self.m = CoversModel(current_cover, self)
|
||||
self.setModel(self.m)
|
||||
|
||||
self.setFlow(self.LeftToRight)
|
||||
self.setWrapping(True)
|
||||
self.setResizeMode(self.Adjust)
|
||||
self.setGridSize(QSize(190, 260))
|
||||
self.setIconSize(QSize(150, 200))
|
||||
self.setSelectionMode(self.SingleSelection)
|
||||
self.setViewMode(self.IconMode)
|
||||
|
||||
self.delegate = CoverDelegate(self)
|
||||
self.setItemDelegate(self.delegate)
|
||||
self.delegate.needs_redraw.connect(self.viewport().update,
|
||||
type=Qt.QueuedConnection)
|
||||
|
||||
self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection)
|
||||
|
||||
def select(self, num):
|
||||
current = self.model().index(num)
|
||||
sm = self.selectionModel()
|
||||
sm.select(current, sm.SelectCurrent)
|
||||
|
||||
def start(self):
|
||||
self.select(0)
|
||||
self.delegate.start_animation()
|
||||
|
||||
def clear_failed(self):
|
||||
plugin = self.m.plugin_for_index(self.currentIndex())
|
||||
self.m.clear_failed()
|
||||
self.select(self.m.index_for_plugin(plugin).row())
|
||||
|
||||
# }}}
|
||||
|
||||
class CoversWidget(QWidget): # {{{
|
||||
|
||||
chosen = pyqtSignal()
|
||||
finished = pyqtSignal()
|
||||
|
||||
def __init__(self, log, current_cover, parent=None):
|
||||
QWidget.__init__(self, parent)
|
||||
self.log = log
|
||||
self.abort = Event()
|
||||
|
||||
self.l = l = QGridLayout()
|
||||
self.setLayout(l)
|
||||
|
||||
self.msg = QLabel()
|
||||
self.msg.setWordWrap(True)
|
||||
l.addWidget(self.msg, 0, 0)
|
||||
|
||||
self.covers_view = CoversView(current_cover, self)
|
||||
self.covers_view.chosen.connect(self.chosen)
|
||||
l.addWidget(self.covers_view, 1, 0)
|
||||
self.continue_processing = True
|
||||
|
||||
def start(self, book, current_cover, title, authors):
|
||||
self.book, self.current_cover = book, current_cover
|
||||
self.title, self.authors = title, authors
|
||||
self.log('\n\nStarting cover download for:', book.title)
|
||||
self.msg.setText('<p>'+_('Downloading covers for <b>%s</b>, please wait...')%book.title)
|
||||
self.covers_view.start()
|
||||
|
||||
self.worker = CoverWorker(self.log, self.abort, self.title,
|
||||
self.authors, book.identifiers)
|
||||
self.worker.start()
|
||||
QTimer.singleShot(50, self.check)
|
||||
self.covers_view.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
def check(self):
|
||||
if self.worker.is_alive() and not self.abort.is_set():
|
||||
QTimer.singleShot(50, self.check)
|
||||
try:
|
||||
self.process_result(self.worker.rq.get_nowait())
|
||||
except Empty:
|
||||
pass
|
||||
else:
|
||||
self.process_results()
|
||||
|
||||
def process_results(self):
|
||||
while self.continue_processing:
|
||||
try:
|
||||
self.process_result(self.worker.rq.get_nowait())
|
||||
except Empty:
|
||||
break
|
||||
|
||||
self.covers_view.clear_failed()
|
||||
|
||||
if self.worker.error is not None:
|
||||
error_dialog(self, _('Download failed'),
|
||||
_('Failed to download any covers, click'
|
||||
' "Show details" for details.'),
|
||||
det_msg=self.worker.error, show=True)
|
||||
|
||||
num = self.covers_view.model().rowCount()
|
||||
if num < 2:
|
||||
txt = _('Could not find any covers for <b>%s</b>')%self.book.title
|
||||
else:
|
||||
txt = _('Found <b>%d</b> covers of %s. Pick the one you like'
|
||||
' best.')%(num-1, self.title)
|
||||
self.msg.setText(txt)
|
||||
|
||||
self.finished.emit()
|
||||
|
||||
def process_result(self, result):
|
||||
if not self.continue_processing:
|
||||
return
|
||||
plugin, width, height, fmt, data = result
|
||||
self.covers_view.model().update_result(plugin, width, height, data)
|
||||
|
||||
def cleanup(self):
|
||||
self.covers_view.delegate.stop_animation()
|
||||
self.continue_processing = False
|
||||
|
||||
def cancel(self):
|
||||
self.continue_processing = False
|
||||
self.abort.set()
|
||||
|
||||
def cover_pixmap(self):
|
||||
idx = None
|
||||
for i in self.covers_view.selectionModel().selectedIndexes():
|
||||
if i.isValid():
|
||||
idx = i
|
||||
break
|
||||
if idx is None:
|
||||
idx = self.covers_view.currentIndex()
|
||||
return self.covers_view.model().cover_pixmap(idx)
|
||||
|
||||
# }}}
|
||||
|
||||
class LogViewer(QDialog): # {{{
|
||||
|
||||
def __init__(self, log, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.log = log
|
||||
self.l = l = QVBoxLayout()
|
||||
self.setLayout(l)
|
||||
|
||||
self.tb = QTextBrowser(self)
|
||||
l.addWidget(self.tb)
|
||||
|
||||
self.bb = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
l.addWidget(self.bb)
|
||||
self.bb.rejected.connect(self.reject)
|
||||
self.bb.accepted.connect(self.accept)
|
||||
|
||||
self.setWindowTitle(_('Download log'))
|
||||
self.setWindowIcon(QIcon(I('debug.png')))
|
||||
self.resize(QSize(800, 400))
|
||||
|
||||
self.keep_updating = True
|
||||
self.last_html = None
|
||||
self.finished.connect(self.stop)
|
||||
QTimer.singleShot(1000, self.update_log)
|
||||
|
||||
self.show()
|
||||
|
||||
def stop(self, *args):
|
||||
self.keep_updating = False
|
||||
|
||||
def update_log(self):
|
||||
if not self.keep_updating:
|
||||
return
|
||||
html = self.log.html
|
||||
if html != self.last_html:
|
||||
self.last_html = html
|
||||
self.tb.setHtml('<pre>%s</pre>'%html)
|
||||
QTimer.singleShot(1000, self.update_log)
|
||||
|
||||
# }}}
|
||||
|
||||
class FullFetch(QDialog): # {{{
|
||||
|
||||
def __init__(self, log, current_cover=None, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.log, self.current_cover = log, current_cover
|
||||
self.book = self.cover_pixmap = None
|
||||
|
||||
self.setWindowTitle(_('Downloading metadata...'))
|
||||
self.setWindowIcon(QIcon(I('metadata.png')))
|
||||
@ -213,25 +792,83 @@ class FullFetch(QDialog): # {{{
|
||||
self.setLayout(l)
|
||||
l.addWidget(self.stack)
|
||||
|
||||
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
|
||||
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
||||
l.addWidget(self.bb)
|
||||
self.bb.rejected.connect(self.reject)
|
||||
self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole)
|
||||
self.next_button.setDefault(True)
|
||||
self.next_button.setEnabled(False)
|
||||
self.next_button.setIcon(QIcon(I('ok.png')))
|
||||
self.next_button.clicked.connect(self.next_clicked)
|
||||
self.ok_button = self.bb.button(self.bb.Ok)
|
||||
self.ok_button.clicked.connect(self.ok_clicked)
|
||||
self.log_button = self.bb.addButton(_('View log'), self.bb.ActionRole)
|
||||
self.log_button.clicked.connect(self.view_log)
|
||||
self.log_button.setIcon(QIcon(I('debug.png')))
|
||||
self.ok_button.setVisible(False)
|
||||
|
||||
self.identify_widget = IdentifyWidget(log, self)
|
||||
self.identify_widget.rejected.connect(self.reject)
|
||||
self.identify_widget.results_found.connect(self.identify_results_found)
|
||||
self.identify_widget.book_selected.connect(self.book_selected)
|
||||
self.stack.addWidget(self.identify_widget)
|
||||
self.resize(850, 500)
|
||||
|
||||
self.covers_widget = CoversWidget(self.log, self.current_cover, parent=self)
|
||||
self.covers_widget.chosen.connect(self.ok_clicked)
|
||||
self.stack.addWidget(self.covers_widget)
|
||||
|
||||
self.resize(850, 550)
|
||||
|
||||
self.finished.connect(self.cleanup)
|
||||
|
||||
def view_log(self):
|
||||
self._lv = LogViewer(self.log, self)
|
||||
|
||||
def book_selected(self, book):
|
||||
self.next_button.setVisible(False)
|
||||
self.ok_button.setVisible(True)
|
||||
self.book = book
|
||||
self.stack.setCurrentIndex(1)
|
||||
self.covers_widget.start(book, self.current_cover,
|
||||
self.title, self.authors)
|
||||
|
||||
def accept(self):
|
||||
# Prevent pressing Enter from closing the dialog
|
||||
# Prevent the usual dialog accept mechanisms from working
|
||||
pass
|
||||
|
||||
def reject(self):
|
||||
self.identify_widget.cancel()
|
||||
return QDialog.reject(self)
|
||||
|
||||
def cleanup(self):
|
||||
self.covers_widget.cleanup()
|
||||
|
||||
def identify_results_found(self):
|
||||
self.next_button.setEnabled(True)
|
||||
|
||||
def next_clicked(self, *args):
|
||||
self.identify_widget.get_result()
|
||||
|
||||
def ok_clicked(self, *args):
|
||||
self.cover_pixmap = self.covers_widget.cover_pixmap()
|
||||
if DEBUG_DIALOG:
|
||||
if self.cover_pixmap is not None:
|
||||
self.w = QLabel()
|
||||
self.w.setPixmap(self.cover_pixmap)
|
||||
self.stack.addWidget(self.w)
|
||||
self.stack.setCurrentIndex(2)
|
||||
else:
|
||||
QDialog.accept(self)
|
||||
|
||||
def start(self, title=None, authors=None, identifiers={}):
|
||||
self.title, self.authors = title, authors
|
||||
self.identify_widget.start(title=title, authors=authors,
|
||||
identifiers=identifiers)
|
||||
self.exec_()
|
||||
# }}}
|
||||
|
||||
if __name__ == '__main__':
|
||||
#DEBUG_DIALOG = True
|
||||
app = QApplication([])
|
||||
d = FullFetch(Log())
|
||||
d.start(title='great gatsby', authors=['Fitzgerald'])
|
||||
|
@ -11,7 +11,7 @@ from PyQt4.Qt import Qt, QVariant, QListWidgetItem
|
||||
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting
|
||||
from calibre.gui2.preferences.behavior_ui import Ui_Form
|
||||
from calibre.gui2 import config, info_dialog, dynamic
|
||||
from calibre.gui2 import config, info_dialog, dynamic, gprefs
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.customize.ui import available_output_formats, all_input_formats
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
@ -19,6 +19,7 @@ from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.ebooks.oeb.iterator import is_supported
|
||||
from calibre.constants import iswindows
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.config import test_eight_code
|
||||
|
||||
class OutputFormatSetting(Setting):
|
||||
|
||||
@ -62,6 +63,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
signal = getattr(self.opt_internally_viewed_formats, 'item'+signal)
|
||||
signal.connect(self.internally_viewed_formats_changed)
|
||||
|
||||
r('bools_are_tristate', db.prefs, restart_required=True)
|
||||
if test_eight_code:
|
||||
r = self.register
|
||||
choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')]
|
||||
r('edit_metadata_single_layout', gprefs, choices=choices)
|
||||
else:
|
||||
self.opt_edit_metadata_single_layout.setVisible(False)
|
||||
self.edit_metadata_single_label.setVisible(False)
|
||||
|
||||
def initialize(self):
|
||||
ConfigWidgetBase.initialize(self)
|
||||
|
@ -14,44 +14,92 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<item row="0" column="1">
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>00</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="opt_overwrite_author_title_metadata">
|
||||
<property name="text">
|
||||
<string>&Overwrite author and title by default when fetching metadata</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<item row="0" column="2">
|
||||
<widget class="QCheckBox" name="opt_get_social_metadata">
|
||||
<property name="text">
|
||||
<string>Download &social metadata (tags/ratings/etc.) by default</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="opt_new_version_notification">
|
||||
<property name="text">
|
||||
<string>Show notification when &new version is available</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<item row="2" column="2">
|
||||
<widget class="QCheckBox" name="opt_bools_are_tristate">
|
||||
<property name="text">
|
||||
<string>Yes/No columns have three values (Requires restart)</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>If checked, Yes/No custom columns values can be Yes, No, or Unknown.
|
||||
If not checked, the values can be Yes or No.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="opt_upload_news_to_device">
|
||||
<property name="text">
|
||||
<string>Automatically send downloaded &news to ebook reader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<item row="4" column="2">
|
||||
<widget class="QCheckBox" name="opt_delete_news_from_library_on_upload">
|
||||
<property name="text">
|
||||
<string>&Delete news from library when it is automatically sent to reader</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="1" column="0">
|
||||
<item row="6" column="0">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_23">
|
||||
<property name="text">
|
||||
<string>Preferred &output format:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_output_format</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="opt_output_format">
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||
</property>
|
||||
<property name="minimumContentsLength">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="2">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Default network &timeout:</string>
|
||||
@ -61,7 +109,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item>
|
||||
<widget class="QSpinBox" name="opt_network_timeout">
|
||||
<property name="toolTip">
|
||||
<string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string>
|
||||
@ -80,7 +128,21 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
</layout>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="priority_label">
|
||||
<property name="text">
|
||||
<string>Job &priority:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_worker_process_priority</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="opt_worker_process_priority">
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||
@ -105,37 +167,11 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="priority_label">
|
||||
<property name="text">
|
||||
<string>Job &priority:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_worker_process_priority</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_23">
|
||||
<property name="text">
|
||||
<string>Preferred &output format:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_output_format</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="opt_output_format">
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||
</property>
|
||||
<property name="minimumContentsLength">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<item row="8" column="2">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_170">
|
||||
<property name="text">
|
||||
<string>Restriction to apply when the current library is opened:</string>
|
||||
@ -145,7 +181,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item>
|
||||
<widget class="QComboBox" name="opt_gui_restriction">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
@ -166,14 +202,28 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="reset_confirmation_button">
|
||||
<item row="9" column="0">
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="edit_metadata_single_label">
|
||||
<property name="text">
|
||||
<string>Reset all disabled &confirmation dialogs</string>
|
||||
<string>Edit metadata (single) layout:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_edit_metadata_single_layout</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<item>
|
||||
<widget class="QComboBox" name="opt_edit_metadata_single_layout">
|
||||
<property name="toolTip">
|
||||
<string>Choose a different layout for the Edit Metadata dialog. The compact metadata layout favors editing custom metadata over changing covers and formats.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="20" column="0">
|
||||
<widget class="QGroupBox" name="groupBox_5">
|
||||
<property name="title">
|
||||
<string>Preferred &input format order:</string>
|
||||
@ -235,7 +285,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<item row="20" column="2">
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="title">
|
||||
<string>Use internal &viewer for:</string>
|
||||
@ -254,6 +304,13 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="30" column="0" colspan="3">
|
||||
<widget class="QPushButton" name="reset_confirmation_button">
|
||||
<property name="text">
|
||||
<string>Reset all disabled &confirmation dialogs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
|
@ -49,8 +49,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
r('use_roman_numerals_for_series_number', config)
|
||||
r('separate_cover_flow', config, restart_required=True)
|
||||
|
||||
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
|
||||
(_('Large'), 'large')]
|
||||
choices = [(_('Off'), 'off'), (_('Small'), 'small'),
|
||||
(_('Medium'), 'medium'), (_('Large'), 'large')]
|
||||
r('toolbar_icon_size', gprefs, choices=choices)
|
||||
|
||||
choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'),
|
||||
|
@ -360,6 +360,7 @@ class Preferences(QMainWindow):
|
||||
self.gui.create_device_menu()
|
||||
self.gui.set_device_menu_items_state(bool(self.gui.device_connected))
|
||||
self.gui.tool_bar.build_bar()
|
||||
self.gui.menu_bar.build_bar()
|
||||
self.gui.build_context_menus()
|
||||
self.gui.tool_bar.apply_settings()
|
||||
|
||||
|
@ -18,6 +18,7 @@ from calibre.utils.config import ConfigProxy
|
||||
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
|
||||
Dispatcher, info_dialog
|
||||
from calibre import as_unicode
|
||||
from calibre.utils.icu import sort_key
|
||||
|
||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
@ -42,8 +43,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
else self.opt_password.Password))
|
||||
self.opt_password.setEchoMode(self.opt_password.Password)
|
||||
|
||||
restrictions = sorted(saved_searches().names(),
|
||||
cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
||||
restrictions = sorted(saved_searches().names(), key=sort_key)
|
||||
# verify that the current restriction still exists. If not, clear it.
|
||||
csr = db.prefs.get('cs_restriction', None)
|
||||
if csr and csr not in restrictions:
|
||||
db.prefs.set('cs_restriction', '')
|
||||
choices = [('', '')] + [(x, x) for x in restrictions]
|
||||
r('cs_restriction', db.prefs, choices=choices)
|
||||
|
||||
|
@ -34,9 +34,12 @@ class BaseModel(QAbstractListModel):
|
||||
if name == 'Location Manager':
|
||||
return FakeAction(name, None,
|
||||
_('Switch between library and device views'),
|
||||
dont_remove_from=set(['toolbar-device']))
|
||||
dont_add_to=frozenset(['menubar', 'toolbar',
|
||||
'toolbar-child', 'context-menu',
|
||||
'context-menu-device']))
|
||||
if name is None:
|
||||
return FakeAction('--- '+_('Separator')+' ---', None)
|
||||
return FakeAction('--- '+_('Separator')+' ---', None,
|
||||
dont_add_to=frozenset(['menubar', 'menubar-device']))
|
||||
try:
|
||||
return gui.iactions[name]
|
||||
except:
|
||||
@ -77,6 +80,12 @@ class BaseModel(QAbstractListModel):
|
||||
ans.append(n)
|
||||
return ans
|
||||
|
||||
def has_action(self, name):
|
||||
for a in self._data:
|
||||
if a.name == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class AllModel(BaseModel):
|
||||
|
||||
@ -89,7 +98,7 @@ class AllModel(BaseModel):
|
||||
self._data = self.get_all_actions(current)
|
||||
|
||||
def get_all_actions(self, current):
|
||||
all = list(self.gui.iactions.keys()) + ['Donate']
|
||||
all = list(self.gui.iactions.keys()) + ['Donate', 'Location Manager']
|
||||
all = [x for x in all if x not in current] + [None]
|
||||
all = [self.name_to_action(x, self.gui) for x in all]
|
||||
all = [x for x in all if self.key not in x.dont_add_to]
|
||||
@ -208,12 +217,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
LOCATIONS = [
|
||||
('toolbar', _('The main toolbar')),
|
||||
('toolbar-child', _('The optional second toolbar')),
|
||||
('toolbar-device', _('The main toolbar when a device is connected')),
|
||||
('toolbar-child', _('The optional second toolbar')),
|
||||
('menubar', _('The menubar')),
|
||||
('menubar-device', _('The menubar when a device is connected')),
|
||||
('context-menu', _('The context menu for the books in the '
|
||||
'calibre library')),
|
||||
('context-menu-device', _('The context menu for the books on '
|
||||
'the device'))
|
||||
'the device')),
|
||||
]
|
||||
|
||||
def genesis(self, gui):
|
||||
@ -284,6 +295,18 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.changed_signal.emit()
|
||||
|
||||
def commit(self):
|
||||
# Ensure preferences are showing in either the toolbar or
|
||||
# the menubar.
|
||||
pref_in_toolbar = self.models['toolbar'][1].has_action('Preferences')
|
||||
pref_in_menubar = self.models['menubar'][1].has_action('Preferences')
|
||||
lm_in_toolbar = self.models['toolbar-device'][1].has_action('Location Manager')
|
||||
lm_in_menubar = self.models['menubar-device'][1].has_action('Location Manager')
|
||||
if not pref_in_toolbar and not pref_in_menubar:
|
||||
self.models['menubar'][1].add(['Preferences'])
|
||||
if not lm_in_toolbar and not lm_in_menubar:
|
||||
self.models['menubar-device'][1].add(['Location Manager'])
|
||||
|
||||
# Save data.
|
||||
for am, cm in self.models.values():
|
||||
cm.commit()
|
||||
return False
|
||||
|
@ -153,6 +153,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
|
||||
for ac in self.iactions.values():
|
||||
ac.do_genesis()
|
||||
self.donate_action = QAction(QIcon(I('donate.png')), _('&Donate to support calibre'), self)
|
||||
MainWindowMixin.__init__(self, db)
|
||||
|
||||
# Jobs Button {{{
|
||||
@ -186,8 +187,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
self.system_tray_menu = QMenu(self)
|
||||
self.restore_action = self.system_tray_menu.addAction(
|
||||
QIcon(I('page.png')), _('&Restore'))
|
||||
self.donate_action = self.system_tray_menu.addAction(
|
||||
QIcon(I('donate.png')), _('&Donate to support calibre'))
|
||||
self.system_tray_menu.addAction(self.donate_action)
|
||||
self.donate_button.setDefaultAction(self.donate_action)
|
||||
self.donate_button.setStatusTip(self.donate_button.toolTip())
|
||||
self.eject_action = self.system_tray_menu.addAction(
|
||||
|
@ -547,7 +547,7 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
return matchkind, query
|
||||
|
||||
def get_bool_matches(self, location, query, candidates):
|
||||
bools_are_tristate = tweaks['bool_custom_columns_are_tristate'] != 'no'
|
||||
bools_are_tristate = not self.db_prefs.get('bools_are_tristate')
|
||||
loc = self.field_metadata[location]['rec_index']
|
||||
matches = set()
|
||||
query = icu_lower(query)
|
||||
@ -947,7 +947,7 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
if not fields:
|
||||
fields = [('timestamp', False)]
|
||||
|
||||
keyg = SortKeyGenerator(fields, self.field_metadata, self._data)
|
||||
keyg = SortKeyGenerator(fields, self.field_metadata, self._data, self.db_prefs)
|
||||
self._map.sort(key=keyg)
|
||||
|
||||
tmap = list(itertools.repeat(False, len(self._data)))
|
||||
@ -970,9 +970,10 @@ class SortKey(object):
|
||||
|
||||
class SortKeyGenerator(object):
|
||||
|
||||
def __init__(self, fields, field_metadata, data):
|
||||
def __init__(self, fields, field_metadata, data, db_prefs):
|
||||
from calibre.utils.icu import sort_key
|
||||
self.field_metadata = field_metadata
|
||||
self.db_prefs = db_prefs
|
||||
self.orders = [1 if x[1] else -1 for x in fields]
|
||||
self.entries = [(x[0], field_metadata[x[0]]) for x in fields]
|
||||
self.library_order = tweaks['title_series_sorting'] == 'library_order'
|
||||
@ -1032,7 +1033,7 @@ class SortKeyGenerator(object):
|
||||
val = self.string_sort_key(val)
|
||||
|
||||
elif dt == 'bool':
|
||||
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||
if not self.db_prefs.get('bools_are_tristate'):
|
||||
val = {True: 1, False: 2, None: 2}.get(val, 2)
|
||||
else:
|
||||
val = {True: 1, False: 2, None: 3}.get(val, 3)
|
||||
|
@ -41,7 +41,6 @@ from calibre.utils.magick.draw import save_cover_data_to
|
||||
from calibre.utils.recycle_bin import delete_file, delete_tree
|
||||
from calibre.utils.formatter_functions import load_user_template_functions
|
||||
|
||||
|
||||
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||
|
||||
class Tag(object):
|
||||
@ -214,6 +213,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
defs['gui_restriction'] = defs['cs_restriction'] = ''
|
||||
defs['categories_using_hierarchy'] = []
|
||||
|
||||
# Migrate the bool tristate tweak
|
||||
defs['bools_are_tristate'] = \
|
||||
tweaks.get('bool_custom_columns_are_tristate', 'yes') == 'yes'
|
||||
if self.prefs.get('bools_are_tristate') is None:
|
||||
self.prefs.set('bools_are_tristate', defs['bools_are_tristate'])
|
||||
|
||||
# Migrate saved search and user categories to db preference scheme
|
||||
def migrate_preference(key, default):
|
||||
oldval = prefs[key]
|
||||
|
@ -17,8 +17,8 @@ from calibre.utils.magick.draw import save_cover_data_to, Image, \
|
||||
|
||||
class CSSortKeyGenerator(SortKeyGenerator):
|
||||
|
||||
def __init__(self, fields, fm):
|
||||
SortKeyGenerator.__init__(self, fields, fm, None)
|
||||
def __init__(self, fields, fm, db_prefs):
|
||||
SortKeyGenerator.__init__(self, fields, fm, None, db_prefs)
|
||||
|
||||
def __call__(self, record):
|
||||
return self.itervals(record).next()
|
||||
@ -56,7 +56,8 @@ class ContentServer(object):
|
||||
field = self.db.data.sanitize_sort_field_name(field)
|
||||
if field not in self.db.field_metadata.sortable_field_keys():
|
||||
raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
|
||||
keyg = CSSortKeyGenerator([(field, order)], self.db.field_metadata)
|
||||
keyg = CSSortKeyGenerator([(field, order)], self.db.field_metadata,
|
||||
self.db.prefs)
|
||||
items.sort(key=keyg, reverse=not order)
|
||||
|
||||
# }}}
|
||||
|
@ -197,7 +197,7 @@ Once you've located the zip file of your plugin you can then directly update it
|
||||
|
||||
zip -R /path/to/plugin/zip/file.zip *
|
||||
|
||||
This will automatically update all changed files. It relies on the freely available zip command line tool.
|
||||
This will update all changed files. It relies on the freely available zip command line tool. Note that you should quit calibre before running this command.
|
||||
|
||||
More plugin examples
|
||||
----------------------
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user