diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index c51c8e8503..5251e701b5 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -21,8 +21,7 @@ from calibre.utils.config import config_dir, dynamic, prefs from calibre.utils.date import now, parse_date from calibre.utils.zipfile import ZipFile -#DEBUG = CALIBRE_DEBUG -DEBUG = False +DEBUG = CALIBRE_DEBUG def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None): diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 57859ab501..91c769cbf0 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -12,20 +12,22 @@ from functools import partial from calibre.ebooks.conversion.config import load_defaults from calibre.gui2 import gprefs, open_url, question_dialog +from calibre.utils.config import JSONConfig from calibre.utils.icu import sort_key from catalog_epub_mobi_ui import Ui_Form +from PyQt4 import QtGui from PyQt4.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox, - QDoubleSpinBox, QIcon, QLineEdit, QObject, QRadioButton, QSize, QSizePolicy, - QTableWidget, QTableWidgetItem, QTextEdit, QToolButton, QUrl, - QVBoxLayout, QWidget, + QDoubleSpinBox, QIcon, QInputDialog, QLineEdit, QObject, QRadioButton, + QSize, QSizePolicy, QTableWidget, QTableWidgetItem, QTextEdit, QToolButton, + QUrl, QVBoxLayout, QWidget, SIGNAL) class PluginWidget(QWidget,Ui_Form): TITLE = _('E-book options') HELP = _('Options specific to')+' AZW3/EPUB/MOBI '+_('output') - DEBUG = False + DEBUG = True # Output synced to the connected device? sync_enabled = True @@ -212,8 +214,8 @@ class PluginWidget(QWidget,Ui_Form): else: results = _truncated_results(excluded_tags) finally: - if self.DEBUG: - print(results) + if False and self.DEBUG: + print("exclude_genre_changed(): %s" % results) self.exclude_genre_results.clear() self.exclude_genre_results.setText(results) @@ -239,11 +241,11 @@ class PluginWidget(QWidget,Ui_Form): 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) + self.merge_after.setEnabled(enabled) + self.merge_before.setEnabled(enabled) + self.merge_source_field.setEnabled(enabled) + self.thumb_width.setEnabled(enabled) def generate_genres_changed(self, enabled): ''' @@ -263,6 +265,22 @@ class PluginWidget(QWidget,Ui_Form): self.genre_source_field_name = genre_source_spec['field'] self.exclude_genre_changed() + def get_format_and_title(self): + current_format = None + current_title = None + self.parentWidget().blockSignals(True) + for peer in self.parentWidget().children(): + if peer == self: + continue + elif peer.children(): + for child in peer.children(): + if child.objectName() == 'format': + current_format = str(child.currentText()).strip() + elif child.objectName() == 'title': + current_title = str(child.text()).strip() + self.parentWidget().blockSignals(False) + return current_format, current_title + def header_note_source_field_changed(self,new_index): ''' Process changes in the header_note_source_field combo box @@ -374,15 +392,20 @@ class PluginWidget(QWidget,Ui_Form): # Initialize exclusion rules self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb, - "exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db) + "exclusion_rules_tw", exclusion_rules, self.eligible_custom_fields, self.db) # Initialize prefix rules self.prefix_rules_table = PrefixRules(self.prefix_rules_gb, - "prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db) + "prefix_rules_tw", prefix_rules, self.eligible_custom_fields, self.db) # Initialize excluded genres preview self.exclude_genre_changed() + # Hook Preset signals + self.preset_delete_pb.clicked.connect(self.preset_remove) + self.preset_save_pb.clicked.connect(self.preset_save) + self.preset_field.currentIndexChanged[str].connect(self.preset_change) + def merge_source_field_changed(self,new_index): ''' Process changes in the merge_source_field combo box @@ -404,10 +427,12 @@ class PluginWidget(QWidget,Ui_Form): self.include_hr.setEnabled(False) def options(self): - # Save/return the current options - # exclude_genre stores literally - # Section switches store as True/False - # others store as lists + ''' + Return, optionally save current options + exclude_genre stores literally + Section switches store as True/False + others store as lists + ''' opts_dict = {} prefix_rules_processed = False @@ -469,7 +494,7 @@ class PluginWidget(QWidget,Ui_Form): except: opts_dict['output_profile'] = ['default'] - if self.DEBUG: + if False and self.DEBUG: print "opts_dict" for opt in sorted(opts_dict.keys(), key=sort_key): print " %s: %s" % (opt, repr(opts_dict[opt])) @@ -544,6 +569,214 @@ class PluginWidget(QWidget,Ui_Form): self.genre_source_fields = custom_fields self.genre_source_field.currentIndexChanged.connect(self.genre_source_field_changed) + # Populate the Presets combo box + self.presets = JSONConfig("catalog_presets") + self.preset_field.addItem("") + self.preset_field_values = sorted([p for p in self.presets], key=sort_key) + self.preset_field.addItems(self.preset_field_values) + + def preset_change(self, item_name): + ''' + Update catalog options from current preset + ''' + if not item_name: + return + + current_preset = str(self.preset_field.currentText()) + options = self.presets[current_preset] + + exclusion_rules = [] + prefix_rules = [] + for opt in self.OPTION_FIELDS: + c_name, c_def, c_type = opt + if c_name == 'preset_field': + continue + # Extra entries in options for cli invocation + if c_name in options: + opt_value = options[c_name] + else: + continue + if c_type in ['check_box']: + getattr(self, c_name).setChecked(eval(str(opt_value))) + if c_name == 'generate_genres': + self.genre_source_field.setEnabled(eval(str(opt_value))) + elif c_type in ['combo_box']: + if opt_value is None: + index = 0 + if c_name == 'genre_source_field': + index = self.genre_source_field.findText(_('Tags')) + else: + index = getattr(self,c_name).findText(opt_value) + if index == -1: + if c_name == 'read_source_field': + index = self.read_source_field.findText(_('Tags')) + elif c_name == 'genre_source_field': + index = self.genre_source_field.findText(_('Tags')) + getattr(self,c_name).setCurrentIndex(index) + elif c_type in ['line_edit']: + getattr(self, c_name).setText(opt_value if opt_value else '') + elif c_type in ['radio_button'] and opt_value is not None: + getattr(self, c_name).setChecked(opt_value) + elif c_type in ['spin_box']: + getattr(self, c_name).setValue(float(opt_value)) + if c_type == 'table_widget': + if c_name == 'exclusion_rules_tw': + if opt_value not in exclusion_rules: + exclusion_rules.append(opt_value) + if c_name == 'prefix_rules_tw': + if opt_value not in prefix_rules: + prefix_rules.append(opt_value) + + # Reset exclusion rules + self.exclusion_rules_table.clearLayout() + self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb, + "exclusion_rules_tw", exclusion_rules, self.eligible_custom_fields, self.db) + + # Reset prefix rules + self.prefix_rules_table.clearLayout() + self.prefix_rules_table = PrefixRules(self.prefix_rules_gb, + "prefix_rules_tw", prefix_rules, self.eligible_custom_fields, self.db) + + # Reset excluded genres preview + self.exclude_genre_changed() + + # Reset format and title + format = options['format'] + title = options['catalog_title'] + self.set_format_and_title(format, title) + + def preset_remove(self): + print("preset_remove()") + if self.preset_field.currentIndex() == 0: + return + + if not question_dialog(self, _("Delete saved catalog preset"), + _("The selected saved catalog preset will be deleted. " + "Are you sure?")): + return + + item_id = self.preset_field.currentIndex() + item_name = unicode(self.preset_field.currentText()) + + self.preset_field.blockSignals(True) + self.preset_field.removeItem(item_id) + self.preset_field.blockSignals(False) + self.preset_field.setCurrentIndex(0) + + if item_name in self.presets.keys(): + del(self.presets[item_name]) + self.presets.commit() + + def preset_save(self): + names = [''] + names.extend(self.preset_field_values) + try: + dex = names.index(self.preset_search_name) + except: + dex = 0 + name = '' + while not name: + name, ok = QInputDialog.getItem(self, _('Save catalog preset'), + _('Preset name:'), names, dex, True) + if not ok: + return + if not name: + error_dialog(self, _("Save catalog preset"), + _("You must provide a name."), show=True) + new = True + name = unicode(name) + if name in self.presets.keys(): + if not question_dialog(self, _("Save catalog preset"), + _("That saved preset already exists and will be overwritten. " + "Are you sure?")): + return + new = False + + preset = {} + prefix_rules_processed = False + exclusion_rules_processed = False + + for opt in self.OPTION_FIELDS: + c_name, c_def, c_type = opt + if c_name == 'exclusion_rules_tw' and exclusion_rules_processed: + continue + if c_name == 'prefix_rules_tw' and prefix_rules_processed: + continue + + if c_type in ['check_box', 'radio_button']: + opt_value = getattr(self, c_name).isChecked() + elif c_type in ['combo_box']: + if c_name == 'preset_field': + continue + opt_value = unicode(getattr(self,c_name).currentText()).strip() + elif c_type in ['line_edit']: + opt_value = unicode(getattr(self, c_name).text()).strip() + elif c_type in ['spin_box']: + opt_value = unicode(getattr(self, c_name).value()) + elif c_type in ['table_widget']: + if c_name == 'prefix_rules_tw': + opt_value = self.prefix_rules_table.get_data() + prefix_rules_processed = True + if c_name == 'exclusion_rules_tw': + opt_value = self.exclusion_rules_table.get_data() + exclusion_rules_processed = True + + preset[c_name] = opt_value + # Construct listified version of table rules for cli invocation + if c_name in ['exclusion_rules_tw','prefix_rules_tw']: + self.construct_tw_opts_object(c_name, opt_value, preset) + + format, title = self.get_format_and_title() + preset['format'] = format + preset['catalog_title'] = title + + # Additional items needed for cli invocation + # Generate specs for merge_comments, header_note_source_field, genre_source_field + checked = '' + if self.merge_before.isChecked(): + checked = 'before' + elif self.merge_after.isChecked(): + checked = 'after' + include_hr = self.include_hr.isChecked() + preset['merge_comments_rule'] = "%s:%s:%s" % \ + (self.merge_source_field_name, checked, include_hr) + + preset['header_note_source_field'] = self.header_note_source_field_name + + preset['genre_source_field'] = self.genre_source_field_name + + # Append the output profile + try: + preset['output_profile'] = load_defaults('page_setup')['output_profile'] + except: + preset['output_profile'] = 'default' + + self.presets[name] = preset + self.presets.commit() + + if new: + self.preset_field.blockSignals(True) + self.preset_field.clear() + self.preset_field.addItem('') + self.preset_field_values = sorted([q for q in self.presets], key=sort_key) + self.preset_field.addItems(self.preset_field_values) + self.preset_field.blockSignals(False) + self.preset_field.setCurrentIndex(self.preset_field.findText(name)) + + def set_format_and_title(self, format, title): + for peer in self.parentWidget().children(): + if peer == self: + continue + elif peer.children(): + for child in peer.children(): + if child.objectName() == 'format': + index = child.findText(format) + child.blockSignals(True) + child.setCurrentIndex(index) + child.blockSignals(False) + elif child.objectName() == 'title': + child.setText(title) + def show_help(self): ''' Display help file @@ -631,6 +864,7 @@ class GenericRulesTable(QTableWidget): self.last_row_selected = self.currentRow() self.last_rows_selected = self.selectionModel().selectedRows() + # Add the controls self._init_controls() # Hook check_box changes @@ -681,6 +915,21 @@ class GenericRulesTable(QTableWidget): # In case table was empty self.horizontalHeader().setStretchLastSection(True) + def clearLayout(self): + if self.layout is not None: + old_layout = self.layout + + for child in old_layout.children(): + for i in reversed(range(child.count())): + if child.itemAt(i).widget() is not None: + child.itemAt(i).widget().setParent(None) + import sip + sip.delete(child) + + for i in reversed(range(old_layout.count())): + if old_layout.itemAt(i).widget() is not None: + old_layout.itemAt(i).widget().setParent(None) + def delete_row(self): if self.DEBUG: print("%s:delete_row()" % self.objectName()) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index d212b0aa6f..608c5c81aa 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -20,6 +20,54 @@ Form + + + + + 0 + 0 + + + + Presets + + + + + + + 0 + 0 + + + + Select catalog preset to load + + + + + + + Save current catalog settings as preset + + + Save + + + + + + + Delete current preset + + + Delete + + + + + + @@ -46,6 +94,9 @@ true + + List of books, sorted by Author + &Authors @@ -54,15 +105,21 @@ - + + + List of books, sorted by Title + &Titles - + + + List of series books, sorted by Series + &Series @@ -72,6 +129,9 @@ + + List of books, sorted by Genre + &Genres @@ -80,13 +140,13 @@ - Field containing Genre information + Field containing Genres - + @@ -96,6 +156,9 @@ 26 + + List of books, sorted by date added to calibre + &Recently Added @@ -103,7 +166,7 @@ - + @@ -113,6 +176,9 @@ 26 + + Individual descriptions of books with cover thumbs, sorted by author + &Descriptions @@ -120,6 +186,41 @@ + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + @@ -347,7 +448,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book] - Custom column containing additional content to be merged with Comments metadata. + Custom column containing additional content to be merged with Comments metadata in Descriptions section. @@ -361,7 +462,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book] - Merge additional content before Comments metadata. + Merge additional content before Comments in Descriptions section. &Before @@ -374,7 +475,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book] - Merge additional content after Comments metadata. + Merge additional content after Comments in Descriptions section. &After @@ -394,7 +495,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book] - Separate Comments metadata and additional content with a horizontal rule. + Separate Comments metadata and additional content with a horizontal rule in Descriptions section. Include &Separator @@ -514,7 +615,7 @@ The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Test book] - Custom column source for text to include in Description section. + Custom column source for text to include in Descriptions section. diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index 96290601cd..965394a44e 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -11,17 +11,18 @@ import os from collections import namedtuple from calibre import strftime +from calibre.constants import config_dir from calibre.customize import CatalogPlugin from calibre.customize.conversion import OptionRecommendation, DummyReporter from calibre.ebooks import calibre_cover from calibre.library import current_library_name from calibre.library.catalogs import AuthorSortMismatchException, EmptyCatalogException from calibre.ptempfile import PersistentTemporaryFile +from calibre.utils.config import JSONConfig from calibre.utils.localization import calibre_langcode_to_name, canonicalize_lang, get_lang Option = namedtuple('Option', 'option, default, dest, action, help') - class EPUB_MOBI(CatalogPlugin): 'ePub catalog generator' @@ -162,6 +163,14 @@ 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('--preset', + default=None, + dest='preset', + action=None, + help=_("Use a named preset created with the GUI Catalog builder.\n" + "A preset specifies all settings for building a catalog.\n" + "Default: '%default'\n" + "Applies to AZW3, ePub, MOBI output formats")), Option('--use-existing-cover', default=False, dest='use_existing_cover', @@ -184,6 +193,43 @@ class EPUB_MOBI(CatalogPlugin): from calibre.library.catalogs.epub_mobi_builder import CatalogBuilder from calibre.utils.logging import default_log as log + # If preset specified from the cli, insert stored options from JSON file + if hasattr(opts, 'preset') and opts.preset: + available_presets = JSONConfig("catalog_presets") + if not opts.preset in available_presets: + if available_presets: + print(_('Error: Preset "%s" not found.' % opts.preset)) + print(_('Stored presets: %s' % ', '.join([p for p in sorted(available_presets.keys())]))) + else: + print(_('Error: No stored presets.')) + return 1 + + # Copy the relevant preset values to the opts object + for item in available_presets[opts.preset]: + if not item in ['exclusion_rules_tw', 'format', 'prefix_rules_tw']: + setattr(opts, item, available_presets[opts.preset][item]) + + # Provide an unconnected device + opts.connected_device = { + 'is_device_connected': False, + 'kind': None, + 'name': None, + 'save_template': None, + 'serial': None, + 'storage': None, + } + + # Convert prefix_rules and exclusion_rules from JSON lists to tuples + prs = [] + for rule in opts.prefix_rules: + prs.append(tuple(rule)) + opts.prefix_rules = tuple(prs) + + ers = [] + for rule in opts.exclusion_rules: + ers.append(tuple(rule)) + opts.exclusion_rules = tuple(ers) + opts.log = log opts.fmt = self.fmt = path_to_output.rpartition('.')[2] @@ -329,15 +375,14 @@ class EPUB_MOBI(CatalogPlugin): log.error("incorrect number of args for --exclusion-rules: %s" % repr(rule)) # Display opts - keys = opts_dict.keys() - keys.sort() + keys = sorted(opts_dict.keys()) build_log.append(" opts:") for key in keys: if key in ['catalog_title', 'author_clip', 'connected_kindle', 'creator', 'cross_reference_authors', 'description_clip', 'exclude_book_marker', 'exclude_genre', 'exclude_tags', 'exclusion_rules', 'fmt', 'genre_source_field', 'header_note_source_field', 'merge_comments_rule', - 'output_profile', 'prefix_rules', 'read_book_marker', + 'output_profile', 'prefix_rules', 'preset', 'read_book_marker', 'search_text', 'sort_by', 'sort_descriptions_by_author', 'sync', 'thumb_width', 'use_existing_cover', 'wishlist_tag']: build_log.append(" %s: %s" % (key, repr(opts_dict[key])))