Merge from trunk

This commit is contained in:
Charles Haley 2012-09-07 08:43:12 +02:00
commit 18ee0e2357
104 changed files with 31654 additions and 39464 deletions

View File

@ -19,6 +19,48 @@
# new recipes:
# - title:
- version: 0.8.68
date: 2012-09-07
new features:
- title: "Drivers for the Nokia N9, Viewsonic 7e, Prestigio PER3274B and Coby Kyros 7035 "
tickets: [1046794,1046544]
- title: "Add a tutorial on creating catalogs to the User Manual and a link to it in the create catalogs dialog"
- title: "Wireless device connections: Add an option to force calibre to listen on a particular IP address. Access it by customizing the plugin in Preferences->Plugins"
- title: "Android driver: Add an extra customization option to configure the directory to which ebooks are sent on the storage cards."
tickets: [1045045]
- title: "Add an option under Preferences->Look & Feel->Book Details to hide the cover in the book details panel"
- title: "The Calibre Companion Android app that allows wireless connection of Android device to calibre is out of beta. See https://play.google.com/stor/apps/details?id=com.multipie.calibreandroid"
bug fixes:
- title: "Fix sorting by author not working in the device view in calibre when connected to iTunes"
tickets: [1044619]
- title: "Fix using the 'configure this device' menu action not validating settings"
- title: "Device drivers: Ignore corrupted entries in metadata.calibre, instead of raising an error"
- title: "PDF Output: Do not error out when generating an outline which points to pages that have been removed."
tickets: [1044799]
- title: "PDF Output: Fix incorrect page numbers being generated in the outline when converting some books"
- title: "PDF Output: Reduce memory consumption when writing out the PDF file, by using a stream"
- title: "EPUB metadata: When there are multiple <dc:date> tags use the one with the earliest date as the published date"
improved recipes:
- Wall Street journal (subscription version)
- Houston Chronicle
- Various Romanian news sources
- Business Week Magazine
- Arcamax
- version: 0.8.67
date: 2012-08-31

View File

@ -31,15 +31,13 @@ class Arcamax(BasicNewsRecipe):
, 'language' : language
}
keep_only_tags = [dict(name='div', attrs={'class':['comics-header']}),
dict(name='b', attrs={'class':['current']}),
dict(name='article', attrs={'class':['comic']}),
keep_only_tags = [dict(name='article', attrs={'class':['comic']}),
]
remove_tags = [dict(name='div', attrs={'id':['comicfull' ]}),
dict(name='div', attrs={'class':['calendar' ]}),
dict(name='nav', attrs={'class':['calendar-nav' ]}),
]
#remove_tags = [dict(name='div', attrs={'id':['comicfull' ]}),
#dict(name='div', attrs={'class':['calendar' ]}),
#dict(name='nav', attrs={'class':['calendar-nav' ]}),
#]
def parse_index(self):
feeds = []
@ -48,20 +46,20 @@ class Arcamax(BasicNewsRecipe):
#(u"9 Chickweed Lane", u"http://www.arcamax.com/ninechickweedlane"),
#(u"Agnes", u"http://www.arcamax.com/agnes"),
#(u"Andy Capp", u"http://www.arcamax.com/andycapp"),
(u"BC", u"http://www.arcamax.com/bc"),
(u"BC", u"http://www.arcamax.com/thefunnies/bc"),
#(u"Baby Blues", u"http://www.arcamax.com/babyblues"),
#(u"Beetle Bailey", u"http://www.arcamax.com/beetlebailey"),
(u"Blondie", u"http://www.arcamax.com/blondie"),
(u"Blondie", u"http://www.arcamax.com/thefunnies/blondie"),
#u"Boondocks", u"http://www.arcamax.com/boondocks"),
#(u"Cathy", u"http://www.arcamax.com/cathy"),
#(u"Daddys Home", u"http://www.arcamax.com/daddyshome"),
(u"Dilbert", u"http://www.arcamax.com/dilbert"),
(u"Dilbert", u"http://www.arcamax.com/thefunnies/dilbert"),
#(u"Dinette Set", u"http://www.arcamax.com/thedinetteset"),
(u"Dog Eat Doug", u"http://www.arcamax.com/dogeatdoug"),
(u"Doonesbury", u"http://www.arcamax.com/doonesbury"),
(u"Dog Eat Doug", u"http://www.arcamax.com/thefunnies/dogeatdoug"),
(u"Doonesbury", u"http://www.arcamax.com/thefunnies/doonesbury"),
#(u"Dustin", u"http://www.arcamax.com/dustin"),
(u"Family Circus", u"http://www.arcamax.com/familycircus"),
(u"Garfield", u"http://www.arcamax.com/garfield"),
(u"Family Circus", u"http://www.arcamax.com/thefunnies/familycircus"),
(u"Garfield", u"http://www.arcamax.com/thefunnies/garfield"),
#(u"Get Fuzzy", u"http://www.arcamax.com/getfuzzy"),
#(u"Girls and Sports", u"http://www.arcamax.com/girlsandsports"),
#(u"Hagar the Horrible", u"http://www.arcamax.com/hagarthehorrible"),
@ -70,16 +68,16 @@ class Arcamax(BasicNewsRecipe):
#(u"Luann", u"http://www.arcamax.com/luann"),
#(u"Momma", u"http://www.arcamax.com/momma"),
#(u"Mother Goose and Grimm", u"http://www.arcamax.com/mothergooseandgrimm"),
(u"Mutts", u"http://www.arcamax.com/mutts"),
(u"Mutts", u"http://www.arcamax.com/thefunnies/mutts"),
#(u"Non Sequitur", u"http://www.arcamax.com/nonsequitur"),
#(u"Pearls Before Swine", u"http://www.arcamax.com/pearlsbeforeswine"),
#(u"Pickles", u"http://www.arcamax.com/pickles"),
#(u"Red and Rover", u"http://www.arcamax.com/redandrover"),
#(u"Rubes", u"http://www.arcamax.com/rubes"),
#(u"Rugrats", u"http://www.arcamax.com/rugrats"),
(u"Speed Bump", u"http://www.arcamax.com/speedbump"),
(u"Wizard of Id", u"http://www.arcamax.com/wizardofid"),
(u"Zits", u"http://www.arcamax.com/zits"),
(u"Speed Bump", u"http://www.arcamax.com/thefunnies/speedbump"),
(u"Wizard of Id", u"http://www.arcamax.com/thefunnies/wizardofid"),
(u"Zits", u"http://www.arcamax.com/thefunnies/zits"),
]:
articles = self.make_links(url)
if articles:
@ -93,11 +91,11 @@ class Arcamax(BasicNewsRecipe):
for page in pages:
page_soup = self.index_to_soup(url)
if page_soup:
title = self.tag_to_string(page_soup.find(name='div', attrs={'class':'comics-header'}).h1.contents[0])
title = self.tag_to_string(page_soup.find(name='div', attrs={'class':'columnheader'}).h1.contents[0])
page_url = url
# orig prev_page_url = 'http://www.arcamax.com' + page_soup.find('a', attrs={'class':'prev'}, text='Previous').parent['href']
prev_page_url = 'http://www.arcamax.com' + page_soup.find('span', text='Previous').parent.parent['href']
date = self.tag_to_string(page_soup.find(name='b', attrs={'class':['current']}))
prev_page_url = 'http://www.arcamax.com' + page_soup.find(name='a', attrs={'class':['prev']})['href']
date = self.tag_to_string(page_soup.find(name='span', attrs={'class':['cur']}))
current_articles.append({'title': title, 'url': page_url, 'description':'', 'date': date})
url = prev_page_url
current_articles.reverse()
@ -126,4 +124,5 @@ class Arcamax(BasicNewsRecipe):
img {max-width:100%; min-width:100%;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
'''

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = u'calibre'
numeric_version = (0, 8, 67)
numeric_version = (0, 8, 68)
__version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -291,7 +291,7 @@ class ANDROID(USBMS):
@classmethod
def configure_for_kindle_app(cls):
proxy = cls._configProxy()
proxy['format_map'] = ['mobi', 'azw', 'azw1', 'azw4', 'pdf']
proxy['format_map'] = ['azw3', 'mobi', 'azw', 'azw1', 'azw4', 'pdf']
proxy['use_subdirs'] = False
proxy['extra_customization'] = [
','.join(['kindle']+cls.EBOOK_DIR_MAIN), '']

View File

@ -13,7 +13,8 @@ from calibre.constants import isosx, iswindows
from calibre.devices.errors import OpenFeedback, UserFeedback
from calibre.devices.usbms.deviceconfig import DeviceConfig
from calibre.devices.interface import DevicePlugin
from calibre.ebooks.metadata import authors_to_string, MetaInformation, title_sort
from calibre.ebooks.metadata import (author_to_author_sort, authors_to_string,
MetaInformation, title_sort)
from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.config import config_dir, dynamic, prefs
from calibre.utils.date import now, parse_date
@ -3478,6 +3479,7 @@ class Book(Metadata):
'''
def __init__(self,title,author):
Metadata.__init__(self, title, authors=author.split(' & '))
self.author_sort = author_to_author_sort(author)
@property
def title_sorter(self):

View File

@ -46,9 +46,8 @@ class MTPDeviceBase(DevicePlugin):
def set_progress_reporter(self, report_progress):
self.report_progress = report_progress
@classmethod
def get_gui_name(cls):
return getattr(cls, 'current_friendly_name', cls.gui_name)
def get_gui_name(self):
return getattr(self, 'current_friendly_name', self.gui_name)
def is_usb_connected(self, devices_on_system, debug=False,
only_presence=False):
@ -60,13 +59,4 @@ class MTPDeviceBase(DevicePlugin):
from calibre.devices.utils import build_template_regexp
return build_template_regexp(self.save_template)
@property
def default_save_template(cls):
from calibre.library.save_to_disk import config
return config().parse().send_template
@property
def save_template(self):
# TODO: Use the device specific template here
return self.default_save_template

View File

@ -15,7 +15,7 @@ from calibre import prints
from calibre.constants import iswindows, numeric_version
from calibre.devices.mtp.base import debug
from calibre.ptempfile import SpooledTemporaryFile, PersistentTemporaryDirectory
from calibre.utils.config import from_json, to_json
from calibre.utils.config import from_json, to_json, JSONConfig
from calibre.utils.date import now, isoformat
BASE = importlib.import_module('calibre.devices.mtp.%s.driver'%(
@ -39,9 +39,40 @@ class MTP_DEVICE(BASE):
def __init__(self, *args, **kwargs):
BASE.__init__(self, *args, **kwargs)
self.plugboards = self.plugboard_func = None
self._prefs = None
@property
def prefs(self):
if self._prefs is None:
from calibre.library.save_to_disk import config
self._prefs = p = JSONConfig('mtp_devices')
p.defaults['format_map'] = self.FORMATS
p.defaults['send_to'] = ['eBooks/import',
'wordplayer/calibretransfer', 'Books', 'sdcard/ebooks',
'eBooks', 'kindle']
p.defaults['send_template'] = config().parse().send_template
return self._prefs
def configure_for_kindle_app(self):
proxy = self.prefs
with proxy:
proxy['format_map'] = ['azw3', 'mobi', 'azw', 'azw1', 'azw4', 'pdf']
proxy['send_template'] = '{title} - {authors}'
orig = list(proxy['send_to'])
if 'kindle' in orig:
orig.remove('kindle')
orig.insert(0, 'kindle')
proxy['send_to'] = orig
def configure_for_generic_epub_app(self):
with self.prefs:
for x in ('format_map', 'send_template', 'send_to'):
del self.prefs[x]
def open(self, devices, library_uuid):
self.current_library_uuid = library_uuid
self.location_paths = None
BASE.open(self, devices, library_uuid)
# Device information {{{
@ -248,8 +279,24 @@ class MTP_DEVICE(BASE):
return tuple(x for x in filepath.split('/'))
def prefix_for_location(self, on_card):
# TODO: Implement this
return 'calibre'
if self.location_paths is None:
self.location_paths = {}
for sid, loc in ( (self._main_id, None), (self._carda_id, 'carda'),
(self._cardb_id, 'cardb') ):
if sid is not None:
storage = self.filesystem_cache.storage(sid)
prefixes = self.get_pref('send_to')
p = None
for path in prefixes:
path = path.replace(os.sep, '/')
if storage.find_path(path.split('/')) is not None:
p = path
break
if p is None:
p = 'eBooks'
self.location_paths[loc] = p
return self.location_paths[on_card]
def ensure_parent(self, storage, path):
parent = storage
@ -366,14 +413,28 @@ class MTP_DEVICE(BASE):
# }}}
# Settings {{{
@classmethod
def get_pref(self, key):
return self.prefs.get('device-%s'%self.current_serial_num, {}).get(key,
self.prefs[key])
def config_widget(self):
from calibre.gui2.device_drivers.mtp_config import MTPConfig
return MTPConfig(self)
def save_settings(self, cw):
cw.commit()
def settings(self):
# TODO: Implement this
class Opts(object):
def __init__(s):
s.format_map = self.FORMATS
s.format_map = self.get_pref('format_map')
return Opts()
@property
def save_template(self):
return self.prefs['send_template']
# }}}
if __name__ == '__main__':
@ -390,6 +451,7 @@ if __name__ == '__main__':
dev.set_progress_reporter(prints)
dev.open(cd, None)
dev.filesystem_cache.dump()
print ('Prefix for main mem:', dev.prefix_for_location(None))
finally:
dev.shutdown()

View File

@ -36,13 +36,13 @@ class N770(USBMS):
class N810(N770):
name = 'Nokia 810 Device Interface'
gui_name = 'Nokia 810/900'
gui_name = 'Nokia 810/900/9'
description = _('Communicate with the Nokia 810/900 internet tablet.')
PRODUCT_ID = [0x96, 0x1c7]
PRODUCT_ID = [0x96, 0x1c7, 0x0518]
BCD = [0x316]
WINDOWS_MAIN_MEM = ['N810', 'N900']
WINDOWS_MAIN_MEM = ['N810', 'N900', 'NOKIA_N9']
MAIN_MEMORY_VOLUME_LABEL = 'Nokia Tablet Main Memory'

View File

@ -328,6 +328,19 @@ def info_dialog(parent, title, msg, det_msg='', show=False,
return d.exec_()
return d
def show_restart_warning(msg, parent=None):
d = warning_dialog(parent, _('Restart needed'), msg,
show_copy_button=False)
b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole)
b.setIcon(QIcon(I('lt.png')))
d.do_restart = False
def rf():
d.do_restart = True
b.clicked.connect(rf)
d.set_details('')
d.exec_()
b.clicked.disconnect()
return d.do_restart
class Dispatcher(QObject):

View File

@ -67,7 +67,7 @@ class GenerateCatalogAction(InterfaceAction):
# jobs.results is a list - the first entry is the intended title for the dialog
# Subsequent strings are error messages
dialog_title = job.result.pop(0)
if re.match('warning:', job.result[0].lower()):
if re.search('warning', job.result[0].lower()):
msg = _("Catalog generation complete, with warnings.")
warning_dialog(self.gui, dialog_title, msg, det_msg='\n'.join(job.result), show=True)
else:

View File

@ -6,17 +6,19 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from copy import copy
import re, sys
from functools import partial
from calibre.ebooks.conversion.config import load_defaults
from calibre.gui2 import gprefs, question_dialog
from calibre.gui2 import gprefs, open_url, question_dialog
from calibre.utils.icu import sort_key
from catalog_epub_mobi_ui import Ui_Form
from PyQt4.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox,
QDoubleSpinBox, QIcon, QLineEdit, QObject, QRadioButton, QSize, QSizePolicy,
QTableWidget, QTableWidgetItem, QToolButton, QVBoxLayout, QWidget,
QTableWidget, QTableWidgetItem, QTextEdit, QToolButton, QUrl,
QVBoxLayout, QWidget,
SIGNAL)
class PluginWidget(QWidget,Ui_Form):
@ -44,6 +46,7 @@ class PluginWidget(QWidget,Ui_Form):
LineEditControls = []
RadioButtonControls = []
TableWidgetControls = []
TextEditControls = []
for item in self.__dict__:
if type(self.__dict__[item]) is QCheckBox:
@ -58,6 +61,8 @@ class PluginWidget(QWidget,Ui_Form):
RadioButtonControls.append(str(self.__dict__[item].objectName()))
elif type(self.__dict__[item]) is QTableWidget:
TableWidgetControls.append(str(self.__dict__[item].objectName()))
elif type(self.__dict__[item]) is QTextEdit:
TextEditControls.append(str(self.__dict__[item].objectName()))
option_fields = zip(CheckBoxControls,
[True for i in CheckBoxControls],
@ -72,20 +77,23 @@ class PluginWidget(QWidget,Ui_Form):
# LineEditControls
option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit'])
# TextEditControls
#option_fields += zip(['exclude_genre_results'],['excluded genres will appear here'],['text_edit'])
# SpinBoxControls
option_fields += zip(['thumb_width'],[1.00],['spin_box'])
# Exclusion rules
option_fields += zip(['exclusion_rules_tw','exclusion_rules_tw'],
option_fields += zip(['exclusion_rules_tw'],
[{'ordinal':0,
'enabled':True,
'name':'Catalogs',
'field':'Tags',
'pattern':'Catalog'},],
['table_widget','table_widget'])
['table_widget'])
# Prefix rules
option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'],
option_fields += zip(['prefix_rules_tw','prefix_rules_tw'],
[{'ordinal':0,
'enabled':True,
'name':'Read book',
@ -98,7 +106,7 @@ class PluginWidget(QWidget,Ui_Form):
'field':'Tags',
'pattern':'Wishlist',
'prefix':u'\u00d7'},],
['table_widget','table_widget','table_widget'])
['table_widget','table_widget'])
self.OPTION_FIELDS = option_fields
@ -110,13 +118,13 @@ class PluginWidget(QWidget,Ui_Form):
'''
rule_set = []
for stored_rule in opt_value:
rule = copy(stored_rule)
rule = stored_rule.copy()
# Skip disabled and incomplete rules
if not rule['enabled']:
continue
elif not rule['field'] or not rule['pattern']:
continue
elif 'prefix' in rule and not rule['prefix']:
elif 'prefix' in rule and rule['prefix'] is None:
continue
else:
if rule['field'] != 'Tags':
@ -130,12 +138,58 @@ class PluginWidget(QWidget,Ui_Form):
pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix'])
else:
pr = (rule['name'],rule['field'],rule['pattern'])
rule_set.append(pr)
opt_value = tuple(rule_set)
# Strip off the trailing '_tw'
opts_dict[c_name[:-3]] = opt_value
def fetchEligibleCustomFields(self):
def exclude_genre_changed(self, regex):
""" Dynamically compute excluded genres.
Run exclude_genre regex against db.all_tags() to show excluded tags.
PROVISIONAL CODE, NEEDS TESTING
Args:
regex (QLineEdit.text()): regex to compile, compute
Output:
self.exclude_genre_results (QLabel): updated to show tags to be excluded as genres
"""
results = _('No genres will be excluded')
if not regex:
self.exclude_genre_results.clear()
self.exclude_genre_results.setText(results)
return
try:
pattern = re.compile((str(regex)))
except:
results = _("regex error: %s") % sys.exc_info()[1]
else:
excluded_tags = []
for tag in self.all_tags:
hit = pattern.search(tag)
if hit:
excluded_tags.append(hit.string)
if excluded_tags:
if set(excluded_tags) == set(self.all_tags):
results = _("All genres will be excluded")
else:
results = ', '.join(sorted(excluded_tags))
finally:
if self.DEBUG:
print(results)
self.exclude_genre_results.clear()
self.exclude_genre_results.setText(results)
def exclude_genre_reset(self):
for default in self.OPTION_FIELDS:
if default[0] == 'exclude_genre':
self.exclude_genre.setText(default[1])
break
def fetch_eligible_custom_fields(self):
self.all_custom_fields = self.db.custom_field_keys()
custom_fields = {}
custom_fields['Tags'] = {'field':'tag', 'datatype':u'text'}
@ -146,28 +200,42 @@ class PluginWidget(QWidget,Ui_Form):
'datatype':field_md['datatype']}
self.eligible_custom_fields = custom_fields
def generate_descriptions_changed(self, enabled):
'''
Toggle Description-related controls
'''
self.header_note_source_field.setEnabled(enabled)
self.thumb_width.setEnabled(enabled)
self.merge_source_field.setEnabled(enabled)
self.merge_before.setEnabled(enabled)
self.merge_after.setEnabled(enabled)
self.include_hr.setEnabled(enabled)
def initialize(self, name, db):
'''
CheckBoxControls (c_type: check_box):
['generate_titles','generate_series','generate_genres',
'generate_recently_added','generate_descriptions','include_hr']
'generate_recently_added','generate_descriptions','include_hr']
ComboBoxControls (c_type: combo_box):
['exclude_source_field','header_note_source_field',
'merge_source_field']
'merge_source_field']
LineEditControls (c_type: line_edit):
['exclude_genre']
RadioButtonControls (c_type: radio_button):
['merge_before','merge_after']
['merge_before','merge_after','generate_new_cover', 'use_existing_cover']
SpinBoxControls (c_type: spin_box):
['thumb_width']
TableWidgetControls (c_type: table_widget):
['exclusion_rules_tw','prefix_rules_tw']
TextEditControls (c_type: text_edit):
['exclude_genre_results']
'''
self.name = name
self.db = db
self.fetchEligibleCustomFields()
self.all_tags = db.all_tags()
self.fetch_eligible_custom_fields()
self.populate_combo_boxes()
# Update dialog fields from stored options
@ -200,9 +268,16 @@ class PluginWidget(QWidget,Ui_Form):
if opt_value not in prefix_rules:
prefix_rules.append(opt_value)
# Add icon to the reset button
# Add icon to the reset button, hook textChanged signal
self.reset_exclude_genres_tb.setIcon(QIcon(I('trash.png')))
self.reset_exclude_genres_tb.clicked.connect(self.reset_exclude_genres)
self.reset_exclude_genres_tb.clicked.connect(self.exclude_genre_reset)
# Hook textChanged event for exclude_genre QLineEdit
self.exclude_genre.textChanged.connect(self.exclude_genre_changed)
# Hook Descriptions checkbox for related options, init
self.generate_descriptions.clicked.connect(self.generate_descriptions_changed)
self.generate_descriptions_changed(self.generate_descriptions.isChecked())
# Init self.merge_source_field_name
self.merge_source_field_name = ''
@ -226,6 +301,9 @@ class PluginWidget(QWidget,Ui_Form):
self.prefix_rules_table = PrefixRules(self.prefix_rules_gb,
"prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db)
# Initialize excluded genres preview
self.exclude_genre_changed(unicode(getattr(self, 'exclude_genre').text()).strip())
def options(self):
# Save/return the current options
# exclude_genre stores literally
@ -275,16 +353,21 @@ class PluginWidget(QWidget,Ui_Form):
elif self.merge_after.isChecked():
checked = 'after'
include_hr = self.include_hr.isChecked()
opts_dict['merge_comments'] = "%s:%s:%s" % \
opts_dict['merge_comments_rule'] = "%s:%s:%s" % \
(self.merge_source_field_name, checked, include_hr)
opts_dict['header_note_source_field'] = self.header_note_source_field_name
# Fix up exclude_genre regex if blank. Assume blank = no exclusions
if opts_dict['exclude_genre'] == '':
opts_dict['exclude_genre'] = 'a^'
# Append the output profile
try:
opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']]
except:
opts_dict['output_profile'] = ['default']
if self.DEBUG:
print "opts_dict"
for opt in sorted(opts_dict.keys(), key=sort_key):
@ -377,11 +460,11 @@ class PluginWidget(QWidget,Ui_Form):
self.merge_after.setEnabled(False)
self.include_hr.setEnabled(False)
def reset_exclude_genres(self):
for default in self.OPTION_FIELDS:
if default[0] == 'exclude_genre':
self.exclude_genre.setText(default[1])
break
def show_help(self):
'''
Display help file
'''
open_url(QUrl('http://manual.calibre-ebook.com/catalogs.html'))
class CheckableTableWidgetItem(QTableWidgetItem):
'''

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>650</width>
<width>658</width>
<height>603</height>
</rect>
</property>
@ -41,151 +41,74 @@
<string>Included sections</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1">
<widget class="QCheckBox" name="generate_genres">
<property name="text">
<string>Books by &amp;Genre</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="generate_recently_added">
<property name="text">
<string>Recently &amp;Added</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="generate_descriptions">
<property name="text">
<string>&amp;Descriptions</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="generate_series">
<property name="text">
<string>Books by &amp;Series</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="generate_titles">
<property name="text">
<string>Books by &amp;Title</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="generate_authors">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Books by Author</string>
<string>&amp;Authors</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="generate_titles">
<property name="text">
<string>&amp;Titles</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QCheckBox" name="generate_series">
<property name="text">
<string>&amp;Series</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="generate_genres">
<property name="text">
<string>&amp;Genres</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QCheckBox" name="generate_recently_added">
<property name="text">
<string>&amp;Recently Added</string>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QCheckBox" name="generate_descriptions">
<property name="text">
<string>&amp;Descriptions</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="excludedGenres">
<widget class="QGroupBox" name="prefix_rules_gb">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books.
The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book], and '+', the default tag for a read book.</string>
<string>The first matching prefix rule applies a prefix to book listings in the generated catalog.</string>
</property>
<property name="title">
<string>Excluded genres</string>
<string>Prefixes</string>
</property>
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldsStayAtSizeHint</enum>
</property>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>-1</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Tags to &amp;exclude</string>
</property>
<property name="textFormat">
<enum>Qt::AutoText</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>exclude_genre</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="exclude_genre">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string extracomment="Default: \[[\w]*\]"/>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="reset_exclude_genres_tb">
<property name="toolTip">
<string>Reset to default</string>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6"/>
</item>
</layout>
</widget>
@ -218,22 +141,148 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
</widget>
</item>
<item>
<widget class="QGroupBox" name="prefix_rules_gb">
<widget class="QGroupBox" name="excludedGenres">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>The first matching prefix rule applies a prefix to book listings in the generated catalog.</string>
<string>A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books.
The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book], and '+', the default tag for a read book.</string>
</property>
<property name="title">
<string>Prefixes</string>
<string>Excluded genres</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6"/>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Tags to &amp;exclude (regex):</string>
</property>
<property name="textFormat">
<enum>Qt::AutoText</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>exclude_genre</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="exclude_genre">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string extracomment="Default: \[[\w]*\]"/>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="reset_exclude_genres_tb">
<property name="toolTip">
<string>Reset to default</string>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Results of regex:</string>
</property>
<property name="textFormat">
<enum>Qt::AutoText</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>exclude_genre</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="exclude_genre_results">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Tags that will be excluded as genres</string>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
@ -255,142 +304,9 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
<property name="title">
<string>Other options</string>
</property>
<layout class="QFormLayout" name="formLayout_4">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldsStayAtSizeHint</enum>
</property>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_10">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>&amp;Thumb width</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>merge_source_field</cstring>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="thumb_width">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>137</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Size hint for cover thumbnails included in Descriptions section.</string>
</property>
<property name="suffix">
<string> inch</string>
</property>
<property name="decimals">
<number>2</number>
</property>
<property name="minimum">
<double>1.000000000000000</double>
</property>
<property name="maximum">
<double>2.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>&amp;Extra note</string>
</property>
<property name="buddy">
<cstring>header_note_source_field</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="header_note_source_field">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Custom column source for text to include in Description section.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_9">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>&amp;Merge with Comments</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>merge_source_field</cstring>
</property>
</widget>
</item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="2" column="1">
<layout class="QHBoxLayout" name="merge_with_comments_hl">
<item>
<widget class="QComboBox" name="merge_source_field">
<property name="minimumSize">
@ -419,6 +335,9 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
<property name="text">
<string>&amp;Before</string>
</property>
<attribute name="buttonGroup">
<string>merge_options_bg</string>
</attribute>
</widget>
</item>
<item>
@ -429,6 +348,9 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
<property name="text">
<string>&amp;After</string>
</property>
<attribute name="buttonGroup">
<string>merge_options_bg</string>
</attribute>
</widget>
</item>
<item>
@ -444,7 +366,196 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
<string>Separate Comments metadata and additional content with a horizontal rule.</string>
</property>
<property name="text">
<string>&amp;Separator</string>
<string>Include &amp;Separator</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_9">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>&amp;Merge with Comments:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>merge_source_field</cstring>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="minimumSize">
<size>
<width>175</width>
<height>20</height>
</size>
</property>
<property name="text">
<string>Catalog cover:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="replace_cover_hl">
<item>
<widget class="QRadioButton" name="generate_new_cover">
<property name="text">
<string>Generate new cover</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string>cover_options_bg</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="use_existing_cover">
<property name="text">
<string>Use existing cover</string>
</property>
<attribute name="buttonGroup">
<string>cover_options_bg</string>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="spacer_label">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>E&amp;xtra Description note:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>header_note_source_field</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="header_note_source_field">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Custom column source for text to include in Description section.</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_10">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>&amp;Thumb width:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>merge_source_field</cstring>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="thumb_width">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Size hint for cover thumbnails included in Descriptions section.</string>
</property>
<property name="suffix">
<string> inch</string>
</property>
<property name="decimals">
<number>2</number>
</property>
<property name="minimum">
<double>1.000000000000000</double>
</property>
<property name="maximum">
<double>2.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
@ -457,4 +568,8 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book]
</widget>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="cover_options_bg"/>
<buttongroup name="merge_options_bg"/>
</buttongroups>
</ui>

View File

@ -19,7 +19,8 @@ from calibre.gui2.dialogs.choose_format_device import ChooseFormatDeviceDialog
from calibre.utils.ipc.job import BaseJob
from calibre.devices.scanner import DeviceScanner
from calibre.gui2 import (config, error_dialog, Dispatcher, dynamic,
warning_dialog, info_dialog, choose_dir, FunctionDispatcher)
warning_dialog, info_dialog, choose_dir, FunctionDispatcher,
show_restart_warning)
from calibre.ebooks.metadata import authors_to_string
from calibre import preferred_encoding, prints, force_unicode, as_unicode
from calibre.utils.filenames import ascii_filename
@ -889,12 +890,16 @@ class DeviceMixin(object): # {{{
bb.rejected.connect(d.reject)
l.addWidget(cw)
l.addWidget(bb)
def validate():
if cw.validate():
QDialog.accept(d)
d.accept = validate
if d.exec_() == d.Accepted:
dev.save_settings(cw)
warning_dialog(self, _('Disconnect device'),
_('Disconnect and re-connect the %s for your changes to'
' be applied.')%dev.get_gui_name(), show=True,
show_copy_button=False)
do_restart = show_restart_warning(_('Restart calibre for the changes to %s'
' to be applied.')%dev.get_gui_name(), parent=self)
if do_restart:
self.quit(restart=True)
def _sync_action_triggered(self, *args):
m = getattr(self, '_sync_menu', None)
@ -972,6 +977,7 @@ class DeviceMixin(object): # {{{
connected = False
self.set_device_menu_items_state(connected)
if connected:
self.device_connected = device_kind
self.device_manager.get_device_information(\
FunctionDispatcher(self.info_read))
self.set_default_thumbnail(\
@ -979,9 +985,8 @@ class DeviceMixin(object): # {{{
self.status_bar.show_message(_('Device: ')+\
self.device_manager.device.get_gui_name()+\
_(' detected.'), 3000)
self.device_connected = device_kind
self.library_view.set_device_connected(self.device_connected)
self.refresh_ondevice (reset_only = True)
self.refresh_ondevice(reset_only=True)
else:
self.device_connected = None
self.status_bar.device_disconnected()

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import textwrap
from PyQt4.Qt import QWidget, QListWidgetItem, Qt, QVariant, SIGNAL, \
QLabel, QLineEdit, QCheckBox
from PyQt4.Qt import (QWidget, QListWidgetItem, Qt, QVariant, QLabel,
QLineEdit, QCheckBox)
from calibre.gui2 import error_dialog, question_dialog
from calibre.gui2.device_drivers.configwidget_ui import Ui_ConfigWidget
@ -40,8 +40,8 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
item.setCheckState(Qt.Checked if format in format_map else Qt.Unchecked)
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
self.column_up.clicked.connect(self.up_column)
self.column_down.clicked.connect(self.down_column)
if device.HIDE_FORMATS_CONFIG_BOX:
self.groupBox.hide()
@ -157,3 +157,5 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
'<br>'+unicode(err), show=True)
return False

View File

@ -0,0 +1,239 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import weakref
from PyQt4.Qt import (QWidget, QListWidgetItem, Qt, QToolButton, QLabel,
QTabWidget, QGridLayout, QListWidget, QIcon, QLineEdit, QVBoxLayout,
QPushButton)
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.template_dialog import TemplateDialog
class FormatsConfig(QWidget): # {{{
def __init__(self, all_formats, format_map):
QWidget.__init__(self)
self.l = l = QGridLayout()
self.setLayout(l)
self.f = f = QListWidget(self)
l.addWidget(f, 0, 0, 3, 1)
unchecked_formats = sorted(all_formats - set(format_map))
for fmt in format_map + unchecked_formats:
item = QListWidgetItem(fmt, f)
item.setData(Qt.UserRole, fmt)
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
item.setCheckState(Qt.Checked if fmt in format_map else Qt.Unchecked)
self.button_up = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-up.png')))
l.addWidget(b, 0, 1)
b.clicked.connect(self.up)
self.button_down = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-down.png')))
l.addWidget(b, 2, 1)
b.clicked.connect(self.down)
@property
def format_map(self):
return [unicode(self.f.item(i).data(Qt.UserRole).toString()) for i in
xrange(self.f.count()) if self.f.item(i).checkState()==Qt.Checked]
def validate(self):
if not self.format_map:
error_dialog(self, _('No formats selected'),
_('You must choose at least one format to send to the'
' device'), show=True)
return False
return True
def up(self):
idx = self.f.currentRow()
if idx > 0:
self.f.insertItem(idx-1, self.f.takeItem(idx))
self.f.setCurrentRow(idx-1)
def down(self):
idx = self.f.currentRow()
if idx < self.f.count()-1:
self.f.insertItem(idx+1, self.f.takeItem(idx))
self.f.setCurrentRow(idx+1)
# }}}
class TemplateConfig(QWidget): # {{{
def __init__(self, val):
QWidget.__init__(self)
self.t = t = QLineEdit(self)
t.setText(val or '')
t.setCursorPosition(0)
self.setMinimumWidth(400)
self.l = l = QGridLayout(self)
self.setLayout(l)
self.m = m = QLabel('<p>'+_('''<b>Save &template</b> to control the filename and
location of files sent to the device:'''))
m.setWordWrap(True)
m.setBuddy(t)
l.addWidget(m, 0, 0, 1, 2)
l.addWidget(t, 1, 0, 1, 1)
b = self.b = QPushButton(_('Template editor'))
l.addWidget(b, 1, 1, 1, 1)
b.clicked.connect(self.edit_template)
@property
def template(self):
return unicode(self.t.text()).strip()
def edit_template(self):
t = TemplateDialog(self, self.template)
t.setWindowTitle(_('Edit template'))
if t.exec_():
self.t.setText(t.rule[1])
def validate(self):
from calibre.utils.formatter import validation_formatter
tmpl = self.template
try:
validation_formatter.validate(tmpl)
return True
except Exception as err:
error_dialog(self, _('Invalid template'),
'<p>'+_('The template %s is invalid:')%tmpl + \
'<br>'+unicode(err), show=True)
return False
# }}}
class SendToConfig(QWidget): # {{{
def __init__(self, val):
QWidget.__init__(self)
self.t = t = QLineEdit(self)
t.setText(', '.join(val or []))
t.setCursorPosition(0)
self.l = l = QVBoxLayout(self)
self.setLayout(l)
self.m = m = QLabel('<p>'+_('''A <b>list of &folders</b> on the device to
which to send ebooks. The first one that exists will be used:'''))
m.setWordWrap(True)
m.setBuddy(t)
l.addWidget(m)
l.addWidget(t)
@property
def value(self):
ans = [x.strip() for x in unicode(self.t.text()).strip().split(',')]
return [x for x in ans if x]
# }}}
class MTPConfig(QTabWidget):
def __init__(self, device, parent=None):
QTabWidget.__init__(self, parent)
self._device = weakref.ref(device)
cd = msg = None
if device.current_friendly_name is not None:
if device.current_serial_num is None:
msg = '<p>' + _('The <b>%s</b> device has no serial number, '
'it cannot be configured'%device.current_friendly_name)
else:
cd = 'device-'+device.current_serial_num
else:
msg = '<p>' + _('<b>No MTP device connected.</b><p>'
' You can only configure the MTP device plugin when a device'
' is connected.')
self.current_device_key = cd
if msg:
msg += '<p>' + _('If you want to un-ignore a previously'
' ignored MTP device, use the "Ignored devices" tab.')
l = QLabel(msg)
l.setWordWrap(True)
l.setStyleSheet('QLabel { margin-left: 2em }')
self.insertTab(0, l, _('Cannot configure'))
else:
self.base = QWidget(self)
self.insertTab(0, self.base, _('Configure %s')%self.device.current_friendly_name)
l = self.base.l = QGridLayout(self.base)
self.base.setLayout(l)
self.formats = FormatsConfig(set(BOOK_EXTENSIONS),
self.get_pref('format_map'))
self.send_to = SendToConfig(self.get_pref('send_to'))
self.template = TemplateConfig(self.get_pref('send_template'))
self.base.la = la = QLabel(_('Choose the formats to send to the %s')%self.device.current_friendly_name)
la.setWordWrap(True)
l.addWidget(la, 0, 0, 1, 1)
l.addWidget(self.formats, 1, 0, 3, 1)
l.addWidget(self.send_to, 1, 1, 1, 1)
l.addWidget(self.template, 2, 1, 1, 1)
l.setRowStretch(2, 10)
self.setCurrentIndex(0)
def get_pref(self, key):
p = self.device.prefs.get(self.current_device_key, {})
if not p:
self.device.prefs[self.current_device_key] = p
return p.get(key, self.device.prefs[key])
@property
def device(self):
return self._device()
def validate(self):
if not self.formats.validate():
return False
if not self.template.validate():
return False
return True
def commit(self):
p = self.device.prefs.get(self.current_device_key, {})
p.pop('format_map', None)
f = self.formats.format_map
if f and f != self.device.prefs['format_map']:
p['format_map'] = f
p.pop('send_template', None)
t = self.template.template
if t and t != self.device.prefs['send_template']:
p['send_template'] = t
p.pop('send_to', None)
s = self.send_to.value
if s and s != self.device.prefs['send_to']:
p['send_to'] = s
self.device.prefs[self.current_device_key] = p
if __name__ == '__main__':
from calibre.gui2 import Application
from calibre.devices.mtp.driver import MTP_DEVICE
from calibre.devices.scanner import DeviceScanner
s = DeviceScanner()
s.scan()
app = Application([])
dev = MTP_DEVICE(None)
dev.startup()
cd = dev.detect_managed_devices(s.devices)
dev.open(cd, 'test')
cw = dev.config_widget()
cw.show()
app.exec_()
dev.shutdown()

View File

@ -10,7 +10,7 @@ import os, sys, importlib
from calibre.customize.ui import config
from calibre.gui2.dialogs.catalog_ui import Ui_Dialog
from calibre.gui2 import dynamic, ResizableDialog
from calibre.gui2 import dynamic, ResizableDialog, info_dialog
from calibre.customize.ui import catalog_plugins
class Catalog(ResizableDialog, Ui_Dialog):
@ -22,7 +22,6 @@ class Catalog(ResizableDialog, Ui_Dialog):
from PyQt4.uic import compileUi
ResizableDialog.__init__(self, parent)
self.dbspec, self.ids = dbspec, ids
# Display the number of books we've been passed
@ -115,6 +114,7 @@ class Catalog(ResizableDialog, Ui_Dialog):
self.format.currentIndexChanged.connect(self.show_plugin_tab)
self.buttonBox.button(self.buttonBox.Apply).clicked.connect(self.apply)
self.buttonBox.button(self.buttonBox.Help).clicked.connect(self.help)
self.show_plugin_tab(None)
geom = dynamic.get('catalog_window_geom', None)
@ -129,6 +129,10 @@ class Catalog(ResizableDialog, Ui_Dialog):
if cf in pw.formats:
self.tabs.addTab(pw, pw.TITLE)
break
if hasattr(self.tabs.widget(1),'show_help'):
self.buttonBox.button(self.buttonBox.Help).setVisible(True)
else:
self.buttonBox.button(self.buttonBox.Help).setVisible(False)
def format_changed(self, idx):
cf = unicode(self.format.currentText())
@ -165,6 +169,29 @@ class Catalog(ResizableDialog, Ui_Dialog):
self.save_catalog_settings()
return ResizableDialog.accept(self)
def help(self):
'''
To add help functionality for a specific format:
In gui2.catalog.catalog_<format>.py, add the following:
from calibre.gui2 import open_url
from PyQt4.Qt import QUrl
In the PluginWidget() class, add this method:
def show_help(self):
url = 'file:///' + P('catalog/help_<format>.html')
open_url(QUrl(url))
Create the help file at resources/catalog/help_<format>.html
'''
if self.tabs.count() > 1 and hasattr(self.tabs.widget(1),'show_help'):
try:
self.tabs.widget(1).show_help()
except:
info_dialog(self, _('No help available'),
_('No help available for this output format.'),
show_copy_button=False,
show=True)
def reject(self):
dynamic.set('catalog_window_geom', bytearray(self.saveGeometry()))
ResizableDialog.reject(self)

View File

@ -14,7 +14,7 @@
<string>Generate catalog</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<iconset>
<normaloff>:/images/lt.png</normaloff>:/images/lt.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
@ -37,7 +37,7 @@
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
<set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
@ -54,8 +54,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>666</width>
<height>599</height>
<width>650</width>
<height>575</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">

View File

@ -9,14 +9,14 @@ import textwrap
from functools import partial
from collections import OrderedDict
from PyQt4.Qt import QMainWindow, Qt, QIcon, QStatusBar, QFont, QWidget, \
QScrollArea, QStackedWidget, QVBoxLayout, QLabel, QFrame, QKeySequence, \
QToolBar, QSize, pyqtSignal, QPixmap, QToolButton, QAction, \
QDialogButtonBox, QHBoxLayout
from PyQt4.Qt import (QMainWindow, Qt, QIcon, QStatusBar, QFont, QWidget,
QScrollArea, QStackedWidget, QVBoxLayout, QLabel, QFrame, QKeySequence,
QToolBar, QSize, pyqtSignal, QPixmap, QToolButton, QAction,
QDialogButtonBox, QHBoxLayout)
from calibre.constants import __appname__, __version__, islinux
from calibre.gui2 import gprefs, min_available_height, available_width, \
warning_dialog
from calibre.gui2 import (gprefs, min_available_height, available_width,
show_restart_warning)
from calibre.gui2.preferences import init_gui, AbortCommit, get_plugin
from calibre.customize.ui import preferences_plugins
@ -346,19 +346,8 @@ class Preferences(QMainWindow):
'restarted immediately. You will not be allowed to '
'set any more preferences, until you restart.')
do_restart = show_restart_warning(msg, parent=self)
d = warning_dialog(self, _('Restart needed'), msg,
show_copy_button=False)
b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole)
b.setIcon(QIcon(I('lt.png')))
d.do_restart = False
def rf():
d.do_restart = True
b.clicked.connect(rf)
d.set_details('')
d.exec_()
b.clicked.disconnect()
do_restart = d.do_restart
self.showing_widget.refresh_gui(self.gui)
self.hide_plugin()
if self.close_after_initial or (must_restart and rc) or do_restart:

View File

@ -267,7 +267,7 @@ class Android(Device):
def commit(cls):
super(Android, cls).commit()
for plugin in device_plugins(include_disabled=True):
if plugin.name == 'Android driver':
if hasattr(plugin, 'configure_for_generic_epub_app'):
plugin.configure_for_generic_epub_app()
class AndroidTablet(Android):
@ -287,7 +287,7 @@ class AndroidPhoneWithKindle(Android):
def commit(cls):
super(Android, cls).commit()
for plugin in device_plugins(include_disabled=True):
if plugin.name == 'Android driver':
if hasattr(plugin, 'configure_for_kindle_app'):
plugin.configure_for_kindle_app()
class AndroidTabletWithKindle(AndroidPhoneWithKindle):

View File

@ -17,4 +17,6 @@ FIELDS = ['all', 'title', 'title_sort', 'author_sort', 'authors', 'comments',
TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate', 'title_sort',
'publisher', 'series_index', 'series', 'tags', 'timestamp', 'title', 'uuid' ]
class AuthorSortMismatchException(Exception): pass
class EmptyCatalogException(Exception): pass

View File

@ -7,13 +7,14 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
import os, shutil
from collections import namedtuple
from calibre import strftime
from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks import calibre_cover
from calibre.library.catalogs import AuthorSortMismatchException, EmptyCatalogException
from calibre.ptempfile import PersistentTemporaryFile
Option = namedtuple('Option', 'option, default, dest, action, help')
@ -120,9 +121,9 @@ class EPUB_MOBI(CatalogPlugin):
help=_("Custom field containing note text to insert in Description header.\n"
"Default: '%default'\n"
"Applies to: AZW3, ePub, MOBI output formats")),
Option('--merge-comments',
Option('--merge-comments-rule',
default='::',
dest='merge_comments',
dest='merge_comments_rule',
action = None,
help=_("#<custom field>:[before|after]:[True|False] specifying:\n"
" <custom field> Custom field containing notes to merge with Comments\n"
@ -146,6 +147,13 @@ class EPUB_MOBI(CatalogPlugin):
"When multiple rules are defined, the first matching rule will be used.\n"
"Default:\n" + '"' + '%default' + '"' + "\n"
"Applies to AZW3, ePub, MOBI output formats")),
Option('--use-existing-cover',
default=False,
dest='use_existing_cover',
action = 'store_true',
help=_("Replace existing cover when generating the catalog.\n"
"Default: '%default'\n"
"Applies to: AZW3, ePub, MOBI output formats")),
Option('--thumb-width',
default='1.0',
dest='thumb_width',
@ -182,8 +190,8 @@ class EPUB_MOBI(CatalogPlugin):
else:
op = "kindle"
opts.descriptionClip = 380 if op.endswith('dx') or 'kindle' not in op else 100
opts.authorClip = 100 if op.endswith('dx') or 'kindle' not in op else 60
opts.description_clip = 380 if op.endswith('dx') or 'kindle' not in op else 100
opts.author_clip = 100 if op.endswith('dx') or 'kindle' not in op else 60
opts.output_profile = op
opts.basename = "Catalog"
@ -198,11 +206,12 @@ class EPUB_MOBI(CatalogPlugin):
(self.name,self.fmt,'for %s ' % opts.output_profile if opts.output_profile else '',
'CLI' if opts.cli_environment else 'GUI'))
# If exclude_genre is blank, assume user wants all genre tags included
# If exclude_genre is blank, assume user wants all tags as genres
if opts.exclude_genre.strip() == '':
opts.exclude_genre = '\[^.\]'
build_log.append(" converting empty exclude_genre to '\[^.\]'")
#opts.exclude_genre = '\[^.\]'
#build_log.append(" converting empty exclude_genre to '\[^.\]'")
opts.exclude_genre = 'a^'
build_log.append(" converting empty exclude_genre to 'a^'")
if opts.connected_device['is_device_connected'] and \
opts.connected_device['kind'] == 'device':
if opts.connected_device['serial']:
@ -304,13 +313,13 @@ class EPUB_MOBI(CatalogPlugin):
keys.sort()
build_log.append(" opts:")
for key in keys:
if key in ['catalog_title','authorClip','connected_kindle','descriptionClip',
if key in ['catalog_title','author_clip','connected_kindle','description_clip',
'exclude_book_marker','exclude_genre','exclude_tags',
'exclusion_rules',
'header_note_source_field','merge_comments',
'exclusion_rules', 'fmt',
'header_note_source_field','merge_comments_rule',
'output_profile','prefix_rules','read_book_marker',
'search_text','sort_by','sort_descriptions_by_author','sync',
'thumb_width','wishlist_tag']:
'thumb_width','use_existing_cover','wishlist_tag']:
build_log.append(" %s: %s" % (key, repr(opts_dict[key])))
if opts.verbose:
@ -323,26 +332,30 @@ class EPUB_MOBI(CatalogPlugin):
if opts.verbose:
log.info(" Begin catalog source generation")
catalog.createDirectoryStructure()
catalog.copyResources()
catalog.calculateThumbnailSize()
catalog_source_built = catalog.buildSources()
if opts.verbose:
if catalog_source_built:
try:
catalog.build_sources()
if opts.verbose:
log.info(" Completed catalog source generation\n")
else:
log.error(" *** Terminated catalog generation, check log for details ***")
except (AuthorSortMismatchException, EmptyCatalogException), e:
log.error(" *** Terminated catalog generation: %s ***" % e)
except:
log.error(" unhandled exception in catalog generator")
raise
if catalog_source_built:
else:
recommendations = []
recommendations.append(('remove_fake_margins', False,
OptionRecommendation.HIGH))
recommendations.append(('comments', '', OptionRecommendation.HIGH))
# >>> Use to debug generated catalog code before conversion <<<
if False:
setattr(opts,'debug_pipeline',os.path.expanduser("~/Desktop/Catalog debug"))
"""
>>> Use to debug generated catalog code before pipeline conversion <<<
"""
GENERATE_DEBUG_EPUB = False
if GENERATE_DEBUG_EPUB:
catalog_debug_path = os.path.join(os.path.expanduser('~'),'Desktop','Catalog debug')
setattr(opts,'debug_pipeline',os.path.expanduser(catalog_debug_path))
dp = getattr(opts, 'debug_pipeline', None)
if dp is not None:
@ -357,9 +370,9 @@ class EPUB_MOBI(CatalogPlugin):
recommendations.append(('book_producer',opts.output_profile,
OptionRecommendation.HIGH))
# If cover exists, use it
# Use existing cover or generate new cover
cpath = None
generate_new_cover = False
existing_cover = False
try:
search_text = 'title:"%s" author:%s' % (
opts.catalog_title.replace('"', '\\"'), 'calibre')
@ -367,19 +380,18 @@ class EPUB_MOBI(CatalogPlugin):
if matches:
cpath = db.cover(matches[0], index_is_id=True, as_path=True)
if cpath and os.path.exists(cpath):
recommendations.append(('cover', cpath,
OptionRecommendation.HIGH))
log.info("using existing cover")
else:
log.info("no existing cover, generating new cover")
generate_new_cover = True
else:
log.info("no existing cover, generating new cover")
generate_new_cover = True
existing_cover = True
except:
pass
if generate_new_cover:
if self.opts.use_existing_cover and not existing_cover:
log.warning("no existing catalog cover found")
if self.opts.use_existing_cover and existing_cover:
recommendations.append(('cover', cpath, OptionRecommendation.HIGH))
log.info("using existing catalog cover")
else:
log.info("replacing catalog cover")
new_cover_path = PersistentTemporaryFile(suffix='.jpg')
new_cover = calibre_cover(opts.catalog_title.replace('"', '\\"'), 'calibre')
new_cover_path.write(new_cover)
@ -388,7 +400,7 @@ class EPUB_MOBI(CatalogPlugin):
# Run ebook-convert
from calibre.ebooks.conversion.plumber import Plumber
plumber = Plumber(os.path.join(catalog.catalogPath,
plumber = Plumber(os.path.join(catalog.catalog_path,
opts.basename + '.opf'), path_to_output, log, report_progress=notification,
abort_after_input_dump=False)
plumber.merge_ui_recommendations(recommendations)
@ -399,6 +411,13 @@ class EPUB_MOBI(CatalogPlugin):
except:
pass
if GENERATE_DEBUG_EPUB:
from calibre.ebooks.tweak import zip_rebuilder
input_path = os.path.join(catalog_debug_path,'input')
shutil.copy(P('catalog/mimetype'),input_path)
shutil.copytree(P('catalog/META-INF'),os.path.join(input_path,'META-INF'))
zip_rebuilder(input_path, os.path.join(catalog_debug_path,'input.epub'))
# returns to gui2.actions.catalog:catalog_generated()
return catalog.error

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

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