Sync to trunk.

This commit is contained in:
John Schember 2011-04-09 20:02:56 -04:00
commit 0f62ea111c
114 changed files with 40805 additions and 34819 deletions

View File

@ -19,6 +19,65 @@
# new recipes: # new recipes:
# - title: # - 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 - version: 0.7.53
date: 2011-04-01 date: 2011-04-01

62
recipes/al_ahram.recipe Normal file
View 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
View 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;}
'''

View File

@ -18,7 +18,8 @@ class Economist(BasicNewsRecipe):
__author__ = "Kovid Goyal" __author__ = "Kovid Goyal"
INDEX = 'http://www.economist.com/printedition' 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 oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'

View File

@ -11,7 +11,8 @@ class Economist(BasicNewsRecipe):
language = 'en' language = 'en'
__author__ = "Kovid Goyal" __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.') ' Much slower than the print edition based version.')
oldest_article = 7.0 oldest_article = 7.0

View File

@ -11,7 +11,8 @@ from calibre.web.feeds.news import BasicNewsRecipe
class FinancialTimes(BasicNewsRecipe): class FinancialTimes(BasicNewsRecipe):
title = u'Financial Times' title = u'Financial Times'
__author__ = 'Darko Miletic and Sujata Raman' __author__ = 'Darko Miletic and Sujata Raman'
description = 'Financial world news' description = ('Financial world news. Available after 5AM '
'GMT, daily.')
oldest_article = 2 oldest_article = 2
language = 'en' language = 'en'

View File

@ -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}' 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 #: Specify columns to sort the booklist by on startup
# Provide a set of columns to be sorted on when calibre starts # Provide a set of columns to be sorted on when calibre starts
# The argument is None if saved sort history is to be used # The argument is None if saved sort history is to be used

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.7.53' __version__ = '0.7.54'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re, importlib import re, importlib

View File

@ -453,12 +453,15 @@ def epub_fixers():
# Metadata sources2 {{{ # Metadata sources2 {{{
def metadata_plugins(capabilities): def metadata_plugins(capabilities):
capabilities = frozenset(capabilities) capabilities = frozenset(capabilities)
for plugin in _initialized_plugins: for plugin in all_metadata_plugins():
if isinstance(plugin, Source) and \ if plugin.capabilities.intersection(capabilities) and \
plugin.capabilities.intersection(capabilities) and \
not is_disabled(plugin): not is_disabled(plugin):
yield plugin yield plugin
def all_metadata_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, Source):
yield plugin
# }}} # }}}
# Initialize plugins {{{ # Initialize plugins {{{

View File

@ -26,9 +26,9 @@ class EDGE(USBMS):
PRODUCT_ID = [0x0c02] PRODUCT_ID = [0x0c02]
BCD = [0x0223] BCD = [0x0223]
VENDOR_NAME = 'ANDROID' VENDOR_NAME = ['ANDROID', 'LINUX']
WINDOWS_MAIN_MEM = '__FILE-STOR_GADG' WINDOWS_MAIN_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET']
WINDOWS_CARD_A_MEM = '__FILE-STOR_GADG' WINDOWS_CARD_A_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET']
MAIN_MEMORY_VOLUME_LABEL = 'Edge Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'Edge Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'Edge Storage Card' STORAGE_CARD_VOLUME_LABEL = 'Edge Storage Card'

View File

@ -279,7 +279,7 @@ class Worker(Thread): # Get details {{{
class Amazon(Source): class Amazon(Source):
name = 'Amazon Store' name = 'Amazon Web'
description = _('Downloads metadata from Amazon') description = _('Downloads metadata from Amazon')
capabilities = frozenset(['identify', 'cover']) capabilities = frozenset(['identify', 'cover'])
@ -295,6 +295,14 @@ class Amazon(Source):
'uk' : _('UK'), '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={}): # {{{ def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
domain = self.prefs.get('domain', 'com') domain = self.prefs.get('domain', 'com')

View File

@ -301,6 +301,13 @@ class Source(Plugin):
# Metadata API {{{ # 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): def get_cached_cover_url(self, identifiers):
''' '''
Return cached cover URL for the book identified by Return cached cover URL for the book identified by

View File

@ -167,6 +167,12 @@ class GoogleBooks(Source):
GOOGLE_COVER = 'http://books.google.com/books?id=%s&printsec=frontcover&img=1' 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={}): # {{{ def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
BASE_URL = 'http://books.google.com/books/feeds/volumes?' BASE_URL = 'http://books.google.com/books/feeds/volumes?'
isbn = check_isbn(identifiers.get('isbn', None)) isbn = check_isbn(identifiers.get('isbn', None))

View File

@ -14,7 +14,7 @@ from threading import Thread
from io import BytesIO from io import BytesIO
from operator import attrgetter 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.sources.base import create_log, msprefs
from calibre.ebooks.metadata.xisbn import xisbn from calibre.ebooks.metadata.xisbn import xisbn
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
@ -217,6 +217,10 @@ class ISBNMerge(object):
for r in results: for r in results:
ans.identifiers.update(r.identifiers) 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) # Merge any other fields with no special handling (random merge)
touched_fields = set() touched_fields = set()
for r in results: for r in results:
@ -253,10 +257,10 @@ def identify(log, abort, # {{{
plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()] plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()]
kwargs = { kwargs = {
'title': title, 'title': title,
'authors': authors, 'authors': authors,
'identifiers': identifiers, 'identifiers': identifiers,
'timeout': timeout, 'timeout': timeout,
} }
log('Running identify query with parameters:') log('Running identify query with parameters:')
@ -362,6 +366,18 @@ def identify(log, abort, # {{{
return results 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 {{{ if __name__ == '__main__': # tests {{{
# To run these test use: calibre-debug -e # To run these test use: calibre-debug -e
# src/calibre/ebooks/metadata/sources/identify.py # src/calibre/ebooks/metadata/sources/identify.py

View File

@ -97,6 +97,10 @@ class CSSSelector(etree.XPath):
def __init__(self, css, namespaces=XPNSMAP): def __init__(self, css, namespaces=XPNSMAP):
css = self.MIN_SPACE_RE.sub(r'\1', css) 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: try:
path = css_to_xpath(css) path = css_to_xpath(css)
except UnicodeEncodeError: # Bug in css_to_xpath except UnicodeEncodeError: # Bug in css_to_xpath

View File

@ -13,7 +13,7 @@ from functools import partial
from calibre.ebooks import ConversionError, DRMError from calibre.ebooks import ConversionError, DRMError
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre import isosx, iswindows, islinux, isfreebsd from calibre.constants import isosx, iswindows, islinux, isfreebsd
from calibre import CurrentDir from calibre import CurrentDir
PDFTOHTML = 'pdftohtml' 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 # This is neccessary as pdftohtml doesn't always (linux) respect absolute paths
pdf_path = os.path.abspath(pdf_path) pdf_path = os.path.abspath(pdf_path)
cmd = [PDFTOHTML, '-enc', 'UTF-8', '-noframes', '-p', '-nomerge', '-nodrm', '-q', pdf_path, os.path.basename(index)] cmd = [PDFTOHTML, '-enc', 'UTF-8', '-noframes', '-p', '-nomerge', '-nodrm', '-q', pdf_path, os.path.basename(index)]
if isfreebsd:
cmd.remove('-nodrm')
if no_images: if no_images:
cmd.append('-i') cmd.append('-i')

View File

@ -13,7 +13,7 @@ from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
ORG_NAME = 'KovidsBrain' ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500' 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.config import Config, ConfigProxy, dynamic, JSONConfig
from calibre.utils.localization import set_qt_translator from calibre.utils.localization import set_qt_translator
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
@ -23,21 +23,45 @@ from calibre.utils.date import UNDEFINED_DATE
# Setup gprefs {{{ # Setup gprefs {{{
gprefs = JSONConfig('gui') 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, 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk', 'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences', 'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences',
) )
gprefs.defaults['action-layout-toolbar-device'] = (
gprefs.defaults['action-layout-toolbar-child'] = ()
gprefs.defaults['action-layout-toolbar-device'] = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View',
'Send To Device', None, None, 'Location Manager', None, None, 'Send To Device', None, None, 'Location Manager', None, None,
'Fetch News', 'Save To Disk', 'Connect Share', None, 'Fetch News', 'Save To Disk', 'Connect Share', None,
'Remove Books', None, 'Help', 'Preferences', 'Remove Books', None, 'Help', 'Preferences',
) )
gprefs.defaults['action-layout-toolbar-child'] = ()
gprefs.defaults['action-layout-context-menu'] = ( gprefs.defaults['action-layout-context-menu'] = (
'Edit Metadata', 'Send To Device', 'Save To Disk', 'Edit Metadata', 'Send To Device', 'Save To Disk',
'Connect Share', 'Copy To Library', None, 'Connect Share', 'Copy To Library', None,
@ -57,6 +81,7 @@ gprefs.defaults['toolbar_text'] = 'auto'
gprefs.defaults['font'] = None gprefs.defaults['font'] = None
gprefs.defaults['tags_browser_partition_method'] = 'first letter' gprefs.defaults['tags_browser_partition_method'] = 'first letter'
gprefs.defaults['tags_browser_collapse_at'] = 100 gprefs.defaults['tags_browser_collapse_at'] = 100
gprefs.defaults['edit_metadata_single_layout'] = 'default'
# }}} # }}}

View File

@ -75,7 +75,7 @@ class InterfaceAction(QObject):
dont_remove_from = frozenset([]) dont_remove_from = frozenset([])
all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu', all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu',
'context-menu-device', 'toolbar-child']) 'context-menu-device', 'toolbar-child', 'menubar', 'menubar-device'])
#: Type of action #: Type of action
#: 'current' means acts on the current view #: 'current' means acts on the current view
@ -145,11 +145,10 @@ class InterfaceAction(QObject):
ans[candidate] = zf.read(candidate) ans[candidate] = zf.read(candidate)
return ans return ans
def genesis(self): def genesis(self):
''' '''
Setup this plugin. Only called once during initialization. self.gui is 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``. ``self.qaction``.
''' '''
pass pass

View File

@ -12,7 +12,7 @@ class AddToLibraryAction(InterfaceAction):
name = 'Add To Library' name = 'Add To Library'
action_spec = (_('Add books to library'), 'add_book.png', action_spec = (_('Add books to library'), 'add_book.png',
_('Add books to your calibre library from the connected device'), None) _('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' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -18,6 +18,7 @@ class FetchAnnotationsAction(InterfaceAction):
name = 'Fetch Annotations' name = 'Fetch Annotations'
action_spec = (_('Fetch annotations (experimental)'), None, None, None) action_spec = (_('Fetch annotations (experimental)'), None, None, None)
dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
action_type = 'current' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -18,7 +18,7 @@ class GenerateCatalogAction(InterfaceAction):
name = 'Generate Catalog' name = 'Generate Catalog'
action_spec = (_('Create a catalog of the books in your calibre library'), None, None, None) 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): def generate_catalog(self):
rows = self.gui.library_view.selectionModel().selectedRows() rows = self.gui.library_view.selectionModel().selectedRows()
@ -31,10 +31,10 @@ class GenerateCatalogAction(InterfaceAction):
_('No books selected for catalog generation'), _('No books selected for catalog generation'),
show=True) show=True)
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
dbspec = {} dbspec = {}
for id in ids: for id in ids:
dbspec[id] = {'ondevice': db.ondevice(id, index_is_id=True)} dbspec[id] = {'ondevice': db.ondevice(id, index_is_id=True)}
# Calling gui2.tools:generate_catalog() # Calling gui2.tools:generate_catalog()
ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager, ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager,

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, shutil import os, shutil
from functools import partial 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 import isbytestring
from calibre.constants import filesystem_encoding from calibre.constants import filesystem_encoding
@ -80,7 +80,7 @@ class ChooseLibraryAction(InterfaceAction):
name = 'Choose Library' name = 'Choose Library'
action_spec = (_('%d books'), 'lt.png', action_spec = (_('%d books'), 'lt.png',
_('Choose calibre library to work with'), None) _('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): def genesis(self):
self.count_changed(0) self.count_changed(0)
@ -88,6 +88,9 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.stats = LibraryUsageStats() 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, self.create_action(spec=(_('Switch/create library...'), 'lt.png', None,
None), attr='action_choose') None), attr='action_choose')
self.action_choose.triggered.connect(self.choose_library, self.action_choose.triggered.connect(self.choose_library,
@ -123,6 +126,7 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.choose_menu.addAction(ac) self.choose_menu.addAction(ac)
self.rename_separator = self.choose_menu.addSeparator() self.rename_separator = self.choose_menu.addSeparator()
self.maintenance_menu = QMenu(_('Library Maintenance')) self.maintenance_menu = QMenu(_('Library Maintenance'))
@ -172,6 +176,7 @@ class ChooseLibraryAction(InterfaceAction):
return return
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
locations = list(self.stats.locations(db)) locations = list(self.stats.locations(db))
for ac in self.switch_actions: for ac in self.switch_actions:
ac.setVisible(False) ac.setVisible(False)
self.quick_menu.clear() self.quick_menu.clear()
@ -205,7 +210,6 @@ class ChooseLibraryAction(InterfaceAction):
rename_actions, delete_actions, qs_actions, rename_actions, delete_actions, qs_actions,
self.action_choose) self.action_choose)
def location_selected(self, loc): def location_selected(self, loc):
enabled = loc == 'library' enabled = loc == 'library'
self.qaction.setEnabled(enabled) self.qaction.setEnabled(enabled)

View File

@ -20,7 +20,7 @@ class ConvertAction(InterfaceAction):
name = 'Convert Books' name = 'Convert Books'
action_spec = (_('Convert books'), 'convert.png', None, _('C')) 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' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -24,7 +24,7 @@ class ShareConnMenu(QMenu): # {{{
config_email = pyqtSignal() config_email = pyqtSignal()
toggle_server = 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): def __init__(self, parent=None):
QMenu.__init__(self, parent) QMenu.__init__(self, parent)
@ -121,8 +121,7 @@ class SendToDeviceAction(InterfaceAction):
name = 'Send To Device' name = 'Send To Device'
action_spec = (_('Send to device'), 'sync.png', None, _('D')) action_spec = (_('Send to device'), 'sync.png', None, _('D'))
dont_remove_from = frozenset(['toolbar-device']) dont_add_to = frozenset(['menubar', 'toolbar', 'context-menu', 'toolbar-child'])
dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
def genesis(self): def genesis(self):
self.qaction.triggered.connect(self.do_sync) self.qaction.triggered.connect(self.do_sync)
@ -166,10 +165,14 @@ class ConnectShareAction(InterfaceAction):
def content_server_state_changed(self, running): def content_server_state_changed(self, running):
self.share_conn_menu.server_state_changed(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): def toggle_content_server(self):
if self.gui.content_server is None: if self.gui.content_server is None:
self.gui.start_content_server() self.gui.start_content_server()
else: else:
self.gui.content_server.threaded_exit() self.gui.content_server.threaded_exit()
self.stopping_msg = info_dialog(self.gui, _('Stopping'), self.stopping_msg = info_dialog(self.gui, _('Stopping'),

View File

@ -12,7 +12,7 @@ class EditCollectionsAction(InterfaceAction):
name = 'Edit Collections' name = 'Edit Collections'
action_spec = (_('Manage collections'), None, action_spec = (_('Manage collections'), None,
_('Manage the collections on this device'), 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' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -141,15 +141,18 @@ class EditMetadataAction(InterfaceAction):
list(range(self.gui.library_view.model().rowCount(QModelIndex()))) list(range(self.gui.library_view.model().rowCount(QModelIndex())))
current_row = row_list.index(cr) current_row = row_list.index(cr)
if test_eight_code: func = (self.do_edit_metadata if test_eight_code else
changed = self.do_edit_metadata(row_list, current_row) self.do_edit_metadata_old)
else: changed, rows_to_refresh = func(row_list, current_row)
changed = self.do_edit_metadata_old(row_list, current_row)
m = self.gui.library_view.model()
if rows_to_refresh:
m.refresh_rows(rows_to_refresh)
if changed: if changed:
self.gui.library_view.model().refresh_ids(list(changed)) m.refresh_ids(list(changed))
current = self.gui.library_view.currentIndex() current = self.gui.library_view.currentIndex()
m = self.gui.library_view.model()
if self.gui.cover_flow: if self.gui.cover_flow:
self.gui.cover_flow.dataChanged() self.gui.cover_flow.dataChanged()
m.current_changed(current, previous) m.current_changed(current, previous)
@ -183,6 +186,7 @@ class EditMetadataAction(InterfaceAction):
current_row += d.row_delta current_row += d.row_delta
self.gui.library_view.set_current_row(current_row) self.gui.library_view.set_current_row(current_row)
self.gui.library_view.scroll_to_row(current_row) self.gui.library_view.scroll_to_row(current_row)
return changed, set()
def do_edit_metadata(self, row_list, current_row): def do_edit_metadata(self, row_list, current_row):
from calibre.gui2.metadata.single import edit_metadata 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, changed, rows_to_refresh = edit_metadata(db, row_list, current_row,
parent=self.gui, view_slot=self.view_format_callback, parent=self.gui, view_slot=self.view_format_callback,
set_current_callback=self.set_current_callback) set_current_callback=self.set_current_callback)
return changed return changed, rows_to_refresh
def set_current_callback(self, id_): def set_current_callback(self, id_):
db = self.gui.library_view.model().db db = self.gui.library_view.model().db

View File

@ -11,7 +11,7 @@ class NextMatchAction(InterfaceAction):
name = 'Move to next highlighted book' name = 'Move to next highlighted book'
action_spec = (_('Move to next match'), 'arrow-down.png', action_spec = (_('Move to next match'), 'arrow-down.png',
_('Move to next highlighted match'), [_('N'), _('F3')]) _('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' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -13,7 +13,7 @@ class OpenFolderAction(InterfaceAction):
name = 'Open Folder' name = 'Open Folder'
action_spec = (_('Open containing folder'), 'document_open.png', None, action_spec = (_('Open containing folder'), 'document_open.png', None,
_('O')) _('O'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device']) dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
action_type = 'current' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -16,7 +16,6 @@ class PreferencesAction(InterfaceAction):
name = 'Preferences' name = 'Preferences'
action_spec = (_('Preferences'), 'config.png', None, _('Ctrl+P')) action_spec = (_('Preferences'), 'config.png', None, _('Ctrl+P'))
dont_remove_from = frozenset(['toolbar'])
def genesis(self): def genesis(self):
pm = QMenu() pm = QMenu()

View File

@ -15,7 +15,7 @@ class ShowBookDetailsAction(InterfaceAction):
name = 'Show Book Details' name = 'Show Book Details'
action_spec = (_('Show book details'), 'dialog_information.png', None, action_spec = (_('Show book details'), 'dialog_information.png', None,
_('I')) _('I'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device']) dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
action_type = 'current' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -15,7 +15,7 @@ class TweakEpubAction(InterfaceAction):
action_spec = (_('Tweak ePub'), 'trim.png', action_spec = (_('Tweak ePub'), 'trim.png',
_('Make small changes to ePub format books'), _('Make small changes to ePub format books'),
_('T')) _('T'))
dont_add_to = frozenset(['toolbar-device', 'context-menu-device']) dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
action_type = 'current' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -62,7 +62,7 @@ class Bool(Base):
w = self.widgets[1] w = self.widgets[1]
items = [_('Yes'), _('No'), _('Undefined')] items = [_('Yes'), _('No'), _('Undefined')]
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')] 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] items = items[:-1]
icons = icons[:-1] icons = icons[:-1]
for icon, text in zip(icons, items): for icon, text in zip(icons, items):
@ -70,7 +70,7 @@ class Bool(Base):
def setter(self, val): def setter(self, val):
val = {None: 2, False: 1, True: 0}[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 val = 1
self.widgets[1].setCurrentIndex(val) self.widgets[1].setCurrentIndex(val)
@ -549,7 +549,7 @@ class BulkBool(BulkBase, Bool):
value = None value = None
for book_id in book_ids: for book_id in book_ids:
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) 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 val = False
if value is not None and value != val: if value is not None and value != val:
return None return None
@ -559,7 +559,7 @@ class BulkBool(BulkBase, Bool):
def setup_ui(self, parent): def setup_ui(self, parent):
self.make_widgets(parent, QComboBox) self.make_widgets(parent, QComboBox)
items = [_('Yes'), _('No')] items = [_('Yes'), _('No')]
if tweaks['bool_custom_columns_are_tristate'] == 'no': if not self.db.prefs.get('bools_are_tristate'):
items.append('') items.append('')
else: else:
items.append(_('Undefined')) items.append(_('Undefined'))
@ -571,7 +571,7 @@ class BulkBool(BulkBase, Bool):
def getter(self): def getter(self):
val = self.main_widget.currentIndex() 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] return {2: False, 1: False, 0: True}[val]
else: else:
return {2: None, 1: False, 0: True}[val] return {2: None, 1: False, 0: True}[val]
@ -586,13 +586,13 @@ class BulkBool(BulkBase, Bool):
return return
val = self.gui_val val = self.gui_val
val = self.normalize_ui_val(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 val = False
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
def a_c_checkbox_changed(self): def a_c_checkbox_changed(self):
if not self.ignore_change_signals: 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.main_widget.currentIndex() == 2:
self.a_c_checkbox.setChecked(False) self.a_c_checkbox.setChecked(False)
else: else:

View File

@ -7,12 +7,12 @@ __docformat__ = 'restructuredtext en'
from functools import partial from functools import partial
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \ from PyQt4.Qt import (QIcon, Qt, QWidget, QToolBar, QSize,
pyqtSignal, QToolButton, QMenu, \ pyqtSignal, QToolButton, QMenu, QMenuBar, QAction,
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup 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.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2 import gprefs from calibre.gui2 import gprefs
@ -238,18 +238,117 @@ class Spacer(QWidget): # {{{
self.l.addStretch(10) self.l.addStretch(10)
# }}} # }}}
class ToolBar(QToolBar): # {{{ class MenuAction(QAction): # {{{
def __init__(self, donate, location_manager, child_bar, parent): def __init__(self, clone, parent):
QToolBar.__init__(self, 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.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.setContextMenuPolicy(Qt.PreventContextMenu)
self.setMovable(False) self.setMovable(False)
self.setFloatable(False) self.setFloatable(False)
self.setOrientation(Qt.Horizontal) self.setOrientation(Qt.Horizontal)
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea) self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
self.setStyleSheet('QToolButton:checked { font-weight: bold }') 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.donate_button = donate
self.apply_settings() self.apply_settings()
@ -259,24 +358,20 @@ class ToolBar(QToolBar): # {{{
donate.setCursor(Qt.PointingHandCursor) donate.setCursor(Qt.PointingHandCursor)
self.added_actions = [] self.added_actions = []
self.build_bar() self.build_bar()
self.preferred_width = self.sizeHint().width()
self.setAcceptDrops(True) self.setAcceptDrops(True)
def apply_settings(self): def apply_settings(self):
sz = gprefs['toolbar_icon_size'] 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.setIconSize(QSize(sz, sz))
self.child_bar.setIconSize(QSize(sz, sz)) self.child_bar.setIconSize(QSize(sz, sz))
style = Qt.ToolButtonTextUnderIcon style = Qt.ToolButtonTextUnderIcon
if gprefs['toolbar_text'] == 'never': if sz > 0 and gprefs['toolbar_text'] == 'never':
style = Qt.ToolButtonIconOnly style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style) self.setToolButtonStyle(style)
self.child_bar.setToolButtonStyle(style) self.child_bar.setToolButtonStyle(style)
self.donate_button.set_normal_icon_size(sz, sz) self.donate_button.set_normal_icon_size(sz, sz)
def contextMenuEvent(self, *args):
pass
def build_bar(self): def build_bar(self):
self.showing_donate = False self.showing_donate = False
showing_device = self.location_manager.has_device showing_device = self.location_manager.has_device
@ -284,6 +379,8 @@ class ToolBar(QToolBar): # {{{
mactions = gprefs['action-layout-toolbar'+mactions] mactions = gprefs['action-layout-toolbar'+mactions]
cactions = gprefs['action-layout-toolbar-child'] cactions = gprefs['action-layout-toolbar-child']
show_main = len(mactions) > 0
self.setVisible(show_main)
show_child = len(cactions) > 0 show_child = len(cactions) > 0
self.child_bar.setVisible(show_child) self.child_bar.setVisible(show_child)
@ -309,6 +406,8 @@ class ToolBar(QToolBar): # {{{
self.d_widget = QWidget() self.d_widget = QWidget()
self.d_widget.setLayout(QVBoxLayout()) self.d_widget.setLayout(QVBoxLayout())
self.d_widget.layout().addWidget(self.donate_button) 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) bar.addWidget(self.d_widget)
self.showing_donate = True self.showing_donate = True
elif what in self.gui.iactions: elif what in self.gui.iactions:
@ -316,6 +415,8 @@ class ToolBar(QToolBar): # {{{
bar.addAction(action.qaction) bar.addAction(action.qaction)
self.added_actions.append(action.qaction) self.added_actions.append(action.qaction)
self.setup_tool_button(bar, action.qaction, action.popup_type) 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): def setup_tool_button(self, bar, ac, menu_mode=None):
ch = bar.widgetForAction(ac) ch = bar.widgetForAction(ac)
@ -325,19 +426,7 @@ class ToolBar(QToolBar): # {{{
ch.setAutoRaise(True) ch.setAutoRaise(True)
if ac.menu() is not None and menu_mode is not None: if ac.menu() is not None and menu_mode is not None:
ch.setPopupMode(menu_mode) ch.setPopupMode(menu_mode)
return ch
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)
def database_changed(self, db): def database_changed(self, db):
pass pass
@ -416,11 +505,14 @@ class MainWindowMixin(object): # {{{
self.iactions['Fetch News'].init_scheduler(db) self.iactions['Fetch News'].init_scheduler(db)
self.search_bar = SearchBar(self) self.search_bar = SearchBar(self)
self.child_bar = QToolBar(self) self.child_bar = BaseToolBar(self)
self.tool_bar = ToolBar(self.donate_button, self.tool_bar = ToolBar(self.donate_button,
self.location_manager, self.child_bar, self) self.location_manager, self.child_bar, self)
self.addToolBar(Qt.TopToolBarArea, self.tool_bar) self.addToolBar(Qt.TopToolBarArea, self.tool_bar)
self.addToolBar(Qt.BottomToolBarArea, self.child_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 = self.centralwidget.layout()
l.addWidget(self.search_bar) l.addWidget(self.search_bar)

View File

@ -353,7 +353,7 @@ class CcBoolDelegate(QStyledItemDelegate): # {{{
editor = DelegateCB(parent) editor = DelegateCB(parent)
items = [_('Y'), _('N'), ' '] items = [_('Y'), _('N'), ' ']
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')] 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] items = items[:-1]
icons = icons[:-1] icons = icons[:-1]
for icon, text in zip(icons, items): for icon, text in zip(icons, items):
@ -367,7 +367,7 @@ class CcBoolDelegate(QStyledItemDelegate): # {{{
def setEditorData(self, editor, index): def setEditorData(self, editor, index):
m = index.model() m = index.model()
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']] 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 val = 1 if not val else 0
else: else:
val = 2 if val is None else 1 if not val else 0 val = 2 if val is None else 1 if not val else 0

View File

@ -700,7 +700,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.dc_decorator[col] = functools.partial( self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx, bool_type_decorator, idx=idx,
bool_cols_are_tristate= bool_cols_are_tristate=
tweaks['bool_custom_columns_are_tristate'] != 'no') self.db.prefs.get('bools_are_tristate'))
elif datatype in ('int', 'float'): elif datatype in ('int', 'float'):
self.dc[col] = functools.partial(number_type, idx=idx) self.dc[col] = functools.partial(number_type, idx=idx)
elif datatype == 'datetime': elif datatype == 'datetime':
@ -710,7 +710,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.dc_decorator[col] = functools.partial( self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx, bool_type_decorator, idx=idx,
bool_cols_are_tristate= bool_cols_are_tristate=
tweaks['bool_custom_columns_are_tristate'] != 'no') self.db.prefs.get('bools_are_tristate'))
elif datatype == 'rating': elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx) self.dc[col] = functools.partial(rating_type, idx=idx)
elif datatype == 'series': elif datatype == 'series':

View File

@ -521,7 +521,7 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
# }}} # }}}
class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
cc_two_column = False cc_two_column = False
one_line_comments_toolbar = True 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, def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
set_current_callback=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, d.start(row_list, current_row, view_slot=view_slot,
set_current_callback=set_current_callback) set_current_callback=set_current_callback)
return d.changed, d.rows_to_refresh return d.changed, d.rows_to_refresh

View File

@ -7,26 +7,30 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
DEBUG_DIALOG = False
# Imports {{{
from threading import Thread, Event from threading import Thread, Event
from operator import attrgetter
from Queue import Queue, Empty
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, 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 PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins from calibre.customize.ui import metadata_plugins
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
from calibre.utils.logging import ThreadSafeLog, UnicodeHTMLStream from calibre.utils.logging import GUILog as Log
from calibre.ebooks.metadata.sources.identify import identify from calibre.ebooks.metadata.sources.identify import (identify,
urls_from_identifiers)
class Log(ThreadSafeLog): # {{{ from calibre.ebooks.metadata.book.base import Metadata
from calibre.gui2 import error_dialog, NONE
def __init__(self): from calibre.utils.date import utcnow, fromordinal, format_date
ThreadSafeLog.__init__(self, level=self.DEBUG) from calibre.library.comments import comments_to_html
self.outputs = [UnicodeHTMLStream()] from calibre import force_unicode
def clear(self):
self.outputs[0].clear()
# }}} # }}}
class RichTextDelegate(QStyledItemDelegate): # {{{ class RichTextDelegate(QStyledItemDelegate): # {{{
@ -40,7 +44,11 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
return doc return doc
def sizeHint(self, option, index): 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) ans.setHeight(ans.height()+10)
return ans return ans
@ -56,24 +64,227 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
painter.restore() 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): def __init__(self, parent=None):
QTableView.__init__(self, parent) 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): # {{{ class Comments(QWebView): # {{{
def __init__(self, parent=None): def __init__(self, parent=None):
QWebView.__init__(self, parent) QWebView.__init__(self, parent)
self.setAcceptDrops(False) self.setAcceptDrops(False)
self.setMaximumWidth(270) self.setMaximumWidth(300)
self.setMinimumWidth(270) self.setMinimumWidth(300)
palette = self.palette() palette = self.palette()
palette.setBrush(QPalette.Base, Qt.transparent) palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette) self.page().setPalette(palette)
self.setAttribute(Qt.WA_OpaquePaintEvent, False) 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): def turnoff_scrollbar(self, *args):
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
@ -109,7 +320,7 @@ class Comments(QWebView): # {{{
self.setHtml(templ%html) self.setHtml(templ%html)
# }}} # }}}
class IdentifyWorker(Thread): class IdentifyWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers): def __init__(self, log, abort, title, authors, identifiers):
Thread.__init__(self) Thread.__init__(self)
@ -122,17 +333,42 @@ class IdentifyWorker(Thread):
self.results = [] self.results = []
self.error = None 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): def run(self):
try: try:
self.results = identify(self.log, self.abort, title=self.title, if DEBUG_DIALOG:
authors=self.authors, identifiers=self.identifiers) 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): for i, result in enumerate(self.results):
result.gui_rank = i result.gui_rank = i
except: except:
import traceback 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): def __init__(self, log, parent=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
@ -150,11 +386,15 @@ class IdentifyWidget(QWidget):
l.addWidget(self.top, 0, 0) l.addWidget(self.top, 0, 0)
self.results_view = ResultsView(self) 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) l.addWidget(self.results_view, 1, 0)
self.comments_view = Comments(self) self.comments_view = Comments(self)
l.addWidget(self.comments_view, 1, 1) l.addWidget(self.comments_view, 1, 1)
self.results_view.show_details_signal.connect(self.comments_view.show_data)
self.query = QLabel('download starting...') self.query = QLabel('download starting...')
f = self.query.font() f = self.query.font()
f.setPointSize(f.pointSize()-2) f.setPointSize(f.pointSize()-2)
@ -162,7 +402,7 @@ class IdentifyWidget(QWidget):
self.query.setWordWrap(True) self.query.setWordWrap(True)
l.addWidget(self.query, 2, 0, 1, 2) 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>'+ '<br><span id="dots">.</span></h2>'+
''' '''
<script type="text/javascript"> <script type="text/javascript">
@ -197,13 +437,352 @@ class IdentifyWidget(QWidget):
self.worker = IdentifyWorker(self.log, self.abort, title, self.worker = IdentifyWorker(self.log, self.abort, title,
authors, identifiers) 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): def __init__(self, log, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.log = log 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.setWindowTitle(_('Downloading metadata...'))
self.setWindowIcon(QIcon(I('metadata.png'))) self.setWindowIcon(QIcon(I('metadata.png')))
@ -213,25 +792,83 @@ class FullFetch(QDialog): # {{{
self.setLayout(l) self.setLayout(l)
l.addWidget(self.stack) l.addWidget(self.stack)
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel) self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
l.addWidget(self.bb) l.addWidget(self.bb)
self.bb.rejected.connect(self.reject) 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 = 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.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): def accept(self):
# Prevent pressing Enter from closing the dialog # Prevent the usual dialog accept mechanisms from working
pass 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={}): def start(self, title=None, authors=None, identifiers={}):
self.title, self.authors = title, authors
self.identify_widget.start(title=title, authors=authors, self.identify_widget.start(title=title, authors=authors,
identifiers=identifiers) identifiers=identifiers)
self.exec_() self.exec_()
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__':
#DEBUG_DIALOG = True
app = QApplication([]) app = QApplication([])
d = FullFetch(Log()) d = FullFetch(Log())
d.start(title='great gatsby', authors=['Fitzgerald']) d.start(title='great gatsby', authors=['Fitzgerald'])

View File

@ -11,7 +11,7 @@ from PyQt4.Qt import Qt, QVariant, QListWidgetItem
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting
from calibre.gui2.preferences.behavior_ui import Ui_Form 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.utils.config import prefs
from calibre.customize.ui import available_output_formats, all_input_formats from calibre.customize.ui import available_output_formats, all_input_formats
from calibre.utils.search_query_parser import saved_searches 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.ebooks.oeb.iterator import is_supported
from calibre.constants import iswindows from calibre.constants import iswindows
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.config import test_eight_code
class OutputFormatSetting(Setting): class OutputFormatSetting(Setting):
@ -62,6 +63,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
signal = getattr(self.opt_internally_viewed_formats, 'item'+signal) signal = getattr(self.opt_internally_viewed_formats, 'item'+signal)
signal.connect(self.internally_viewed_formats_changed) 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): def initialize(self):
ConfigWidgetBase.initialize(self) ConfigWidgetBase.initialize(self)

View File

@ -14,44 +14,92 @@
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <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"> <widget class="QCheckBox" name="opt_overwrite_author_title_metadata">
<property name="text"> <property name="text">
<string>&amp;Overwrite author and title by default when fetching metadata</string> <string>&amp;Overwrite author and title by default when fetching metadata</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0" colspan="2"> <item row="0" column="2">
<widget class="QCheckBox" name="opt_get_social_metadata"> <widget class="QCheckBox" name="opt_get_social_metadata">
<property name="text"> <property name="text">
<string>Download &amp;social metadata (tags/ratings/etc.) by default</string> <string>Download &amp;social metadata (tags/ratings/etc.) by default</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0" colspan="2"> <item row="2" column="0">
<widget class="QCheckBox" name="opt_new_version_notification"> <widget class="QCheckBox" name="opt_new_version_notification">
<property name="text"> <property name="text">
<string>Show notification when &amp;new version is available</string> <string>Show notification when &amp;new version is available</string>
</property> </property>
</widget> </widget>
</item> </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"> <widget class="QCheckBox" name="opt_upload_news_to_device">
<property name="text"> <property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string> <string>Automatically send downloaded &amp;news to ebook reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0" colspan="2"> <item row="4" column="2">
<widget class="QCheckBox" name="opt_delete_news_from_library_on_upload"> <widget class="QCheckBox" name="opt_delete_news_from_library_on_upload">
<property name="text"> <property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string> <string>&amp;Delete news from library when it is automatically sent to reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0" colspan="2"> <item row="6" column="0">
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QHBoxLayout">
<item row="1" column="0"> <item>
<widget class="QLabel" name="label_23">
<property name="text">
<string>Preferred &amp;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"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>Default network &amp;timeout:</string> <string>Default network &amp;timeout:</string>
@ -61,7 +109,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item>
<widget class="QSpinBox" name="opt_network_timeout"> <widget class="QSpinBox" name="opt_network_timeout">
<property name="toolTip"> <property name="toolTip">
<string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string> <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> </property>
</widget> </widget>
</item> </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 &amp;priority:</string>
</property>
<property name="buddy">
<cstring>opt_worker_process_priority</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="opt_worker_process_priority"> <widget class="QComboBox" name="opt_worker_process_priority">
<property name="sizeAdjustPolicy"> <property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum> <enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
@ -105,37 +167,11 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="2" column="0"> </layout>
<widget class="QLabel" name="priority_label"> </item>
<property name="text"> <item row="8" column="2">
<string>Job &amp;priority:</string> <layout class="QHBoxLayout">
</property> <item>
<property name="buddy">
<cstring>opt_worker_process_priority</cstring>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Preferred &amp;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">
<widget class="QLabel" name="label_170"> <widget class="QLabel" name="label_170">
<property name="text"> <property name="text">
<string>Restriction to apply when the current library is opened:</string> <string>Restriction to apply when the current library is opened:</string>
@ -145,7 +181,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item>
<widget class="QComboBox" name="opt_gui_restriction"> <widget class="QComboBox" name="opt_gui_restriction">
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
@ -166,14 +202,28 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="6" column="0" colspan="2"> <item row="9" column="0">
<widget class="QPushButton" name="reset_confirmation_button"> <layout class="QHBoxLayout">
<property name="text"> <item>
<string>Reset all disabled &amp;confirmation dialogs</string> <widget class="QLabel" name="edit_metadata_single_label">
</property> <property name="text">
</widget> <string>Edit metadata (single) layout:</string>
</property>
<property name="buddy">
<cstring>opt_edit_metadata_single_layout</cstring>
</property>
</widget>
</item>
<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>
<item row="7" column="0"> <item row="20" column="0">
<widget class="QGroupBox" name="groupBox_5"> <widget class="QGroupBox" name="groupBox_5">
<property name="title"> <property name="title">
<string>Preferred &amp;input format order:</string> <string>Preferred &amp;input format order:</string>
@ -235,7 +285,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="20" column="2">
<widget class="QGroupBox" name="groupBox_3"> <widget class="QGroupBox" name="groupBox_3">
<property name="title"> <property name="title">
<string>Use internal &amp;viewer for:</string> <string>Use internal &amp;viewer for:</string>
@ -254,6 +304,13 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="30" column="0" colspan="3">
<widget class="QPushButton" name="reset_confirmation_button">
<property name="text">
<string>Reset all disabled &amp;confirmation dialogs</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>

View File

@ -49,8 +49,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('use_roman_numerals_for_series_number', config) r('use_roman_numerals_for_series_number', config)
r('separate_cover_flow', config, restart_required=True) r('separate_cover_flow', config, restart_required=True)
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'), choices = [(_('Off'), 'off'), (_('Small'), 'small'),
(_('Large'), 'large')] (_('Medium'), 'medium'), (_('Large'), 'large')]
r('toolbar_icon_size', gprefs, choices=choices) r('toolbar_icon_size', gprefs, choices=choices)
choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'), choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'),

View File

@ -360,6 +360,7 @@ class Preferences(QMainWindow):
self.gui.create_device_menu() self.gui.create_device_menu()
self.gui.set_device_menu_items_state(bool(self.gui.device_connected)) self.gui.set_device_menu_items_state(bool(self.gui.device_connected))
self.gui.tool_bar.build_bar() self.gui.tool_bar.build_bar()
self.gui.menu_bar.build_bar()
self.gui.build_context_menus() self.gui.build_context_menus()
self.gui.tool_bar.apply_settings() self.gui.tool_bar.apply_settings()

View File

@ -18,6 +18,7 @@ from calibre.utils.config import ConfigProxy
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \ from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
Dispatcher, info_dialog Dispatcher, info_dialog
from calibre import as_unicode from calibre import as_unicode
from calibre.utils.icu import sort_key
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
@ -42,8 +43,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
else self.opt_password.Password)) else self.opt_password.Password))
self.opt_password.setEchoMode(self.opt_password.Password) self.opt_password.setEchoMode(self.opt_password.Password)
restrictions = sorted(saved_searches().names(), restrictions = sorted(saved_searches().names(), key=sort_key)
cmp=lambda x,y: cmp(x.lower(), y.lower())) # 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] choices = [('', '')] + [(x, x) for x in restrictions]
r('cs_restriction', db.prefs, choices=choices) r('cs_restriction', db.prefs, choices=choices)

View File

@ -34,9 +34,12 @@ class BaseModel(QAbstractListModel):
if name == 'Location Manager': if name == 'Location Manager':
return FakeAction(name, None, return FakeAction(name, None,
_('Switch between library and device views'), _('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: if name is None:
return FakeAction('--- '+_('Separator')+' ---', None) return FakeAction('--- '+_('Separator')+' ---', None,
dont_add_to=frozenset(['menubar', 'menubar-device']))
try: try:
return gui.iactions[name] return gui.iactions[name]
except: except:
@ -77,6 +80,12 @@ class BaseModel(QAbstractListModel):
ans.append(n) ans.append(n)
return ans return ans
def has_action(self, name):
for a in self._data:
if a.name == name:
return True
return False
class AllModel(BaseModel): class AllModel(BaseModel):
@ -89,7 +98,7 @@ class AllModel(BaseModel):
self._data = self.get_all_actions(current) self._data = self.get_all_actions(current)
def get_all_actions(self, 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 = [x for x in all if x not in current] + [None]
all = [self.name_to_action(x, self.gui) for x in all] 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] 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 = [ LOCATIONS = [
('toolbar', _('The main toolbar')), ('toolbar', _('The main toolbar')),
('toolbar-child', _('The optional second toolbar')),
('toolbar-device', _('The main toolbar when a device is connected')), ('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 ' ('context-menu', _('The context menu for the books in the '
'calibre library')), 'calibre library')),
('context-menu-device', _('The context menu for the books on ' ('context-menu-device', _('The context menu for the books on '
'the device')) 'the device')),
] ]
def genesis(self, gui): def genesis(self, gui):
@ -284,6 +295,18 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit() self.changed_signal.emit()
def commit(self): 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(): for am, cm in self.models.values():
cm.commit() cm.commit()
return False return False

View File

@ -153,6 +153,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
for ac in self.iactions.values(): for ac in self.iactions.values():
ac.do_genesis() ac.do_genesis()
self.donate_action = QAction(QIcon(I('donate.png')), _('&Donate to support calibre'), self)
MainWindowMixin.__init__(self, db) MainWindowMixin.__init__(self, db)
# Jobs Button {{{ # Jobs Button {{{
@ -186,8 +187,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.system_tray_menu = QMenu(self) self.system_tray_menu = QMenu(self)
self.restore_action = self.system_tray_menu.addAction( self.restore_action = self.system_tray_menu.addAction(
QIcon(I('page.png')), _('&Restore')) QIcon(I('page.png')), _('&Restore'))
self.donate_action = self.system_tray_menu.addAction( self.system_tray_menu.addAction(self.donate_action)
QIcon(I('donate.png')), _('&Donate to support calibre'))
self.donate_button.setDefaultAction(self.donate_action) self.donate_button.setDefaultAction(self.donate_action)
self.donate_button.setStatusTip(self.donate_button.toolTip()) self.donate_button.setStatusTip(self.donate_button.toolTip())
self.eject_action = self.system_tray_menu.addAction( self.eject_action = self.system_tray_menu.addAction(

View File

@ -547,7 +547,7 @@ class ResultCache(SearchQueryParser): # {{{
return matchkind, query return matchkind, query
def get_bool_matches(self, location, query, candidates): 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'] loc = self.field_metadata[location]['rec_index']
matches = set() matches = set()
query = icu_lower(query) query = icu_lower(query)
@ -947,7 +947,7 @@ class ResultCache(SearchQueryParser): # {{{
if not fields: if not fields:
fields = [('timestamp', False)] 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) self._map.sort(key=keyg)
tmap = list(itertools.repeat(False, len(self._data))) tmap = list(itertools.repeat(False, len(self._data)))
@ -970,9 +970,10 @@ class SortKey(object):
class SortKeyGenerator(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 from calibre.utils.icu import sort_key
self.field_metadata = field_metadata self.field_metadata = field_metadata
self.db_prefs = db_prefs
self.orders = [1 if x[1] else -1 for x in fields] 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.entries = [(x[0], field_metadata[x[0]]) for x in fields]
self.library_order = tweaks['title_series_sorting'] == 'library_order' self.library_order = tweaks['title_series_sorting'] == 'library_order'
@ -1032,7 +1033,7 @@ class SortKeyGenerator(object):
val = self.string_sort_key(val) val = self.string_sort_key(val)
elif dt == 'bool': 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) val = {True: 1, False: 2, None: 2}.get(val, 2)
else: else:
val = {True: 1, False: 2, None: 3}.get(val, 3) val = {True: 1, False: 2, None: 3}.get(val, 3)

View File

@ -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.recycle_bin import delete_file, delete_tree
from calibre.utils.formatter_functions import load_user_template_functions from calibre.utils.formatter_functions import load_user_template_functions
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
class Tag(object): class Tag(object):
@ -214,6 +213,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
defs['gui_restriction'] = defs['cs_restriction'] = '' defs['gui_restriction'] = defs['cs_restriction'] = ''
defs['categories_using_hierarchy'] = [] 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 # Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default): def migrate_preference(key, default):
oldval = prefs[key] oldval = prefs[key]

View File

@ -17,8 +17,8 @@ from calibre.utils.magick.draw import save_cover_data_to, Image, \
class CSSortKeyGenerator(SortKeyGenerator): class CSSortKeyGenerator(SortKeyGenerator):
def __init__(self, fields, fm): def __init__(self, fields, fm, db_prefs):
SortKeyGenerator.__init__(self, fields, fm, None) SortKeyGenerator.__init__(self, fields, fm, None, db_prefs)
def __call__(self, record): def __call__(self, record):
return self.itervals(record).next() return self.itervals(record).next()
@ -56,7 +56,8 @@ class ContentServer(object):
field = self.db.data.sanitize_sort_field_name(field) field = self.db.data.sanitize_sort_field_name(field)
if field not in self.db.field_metadata.sortable_field_keys(): if field not in self.db.field_metadata.sortable_field_keys():
raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field) 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) items.sort(key=keyg, reverse=not order)
# }}} # }}}

View File

@ -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 * 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 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