From 19eb3ad879b636186b74de3a0a82c6aabb54bf61 Mon Sep 17 00:00:00 2001 From: GRiker Date: Tue, 31 Jul 2012 10:00:17 -0600 Subject: [PATCH 01/11] wip --- src/calibre/library/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index d5defe96c7..4fc268ce7d 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -796,8 +796,10 @@ def catalog_option_parser(args): return parser, plugin, log def command_catalog(args, dbpath): + print("library.cli:command_catalog() EXPERIMENTAL MODE") parser, plugin, log = catalog_option_parser(args) opts, args = parser.parse_args(sys.argv[1:]) + if len(args) < 2: parser.print_help() print From add5bb824d7df55a1454bd0ed7dfbeff33c627dd Mon Sep 17 00:00:00 2001 From: GRiker Date: Fri, 3 Aug 2012 03:23:15 -0600 Subject: [PATCH 02/11] WIP - changes to support prefix rules --- resources/catalog/stylesheet.css | 13 +- src/calibre/gui2/catalog/catalog_epub_mobi.py | 199 +++++++-- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 169 +++----- src/calibre/library/catalogs/epub_mobi.py | 35 +- .../library/catalogs/epub_mobi_builder.py | 388 +++++++++--------- src/calibre/library/cli.py | 2 +- 6 files changed, 437 insertions(+), 369 deletions(-) diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css index 55a7c9fbec..40b0d0eb4e 100644 --- a/resources/catalog/stylesheet.css +++ b/resources/catalog/stylesheet.css @@ -151,14 +151,23 @@ p.title { font-size:xx-large; } -p.wishlist_item, p.unread_book, p.read_book { - text-align:left; +p.wishlist_item, p.unread_book, p.read_book, p.line_item { + font-family:monospace; margin-top:0px; margin-bottom:0px; margin-left:2em; + text-align:left; text-indent:-2em; } +span.prefix {} +span.entry { + font-family: serif; + } + +/* +* Book Descriptions +*/ td.publisher, td.date { font-weight:bold; text-align:center; diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 3cfd94cc6e..7d7119bbf2 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -10,8 +10,11 @@ from calibre.ebooks.conversion.config import load_defaults from calibre.gui2 import gprefs from catalog_epub_mobi_ui import Ui_Form -from PyQt4.Qt import QCheckBox, QComboBox, QDoubleSpinBox, QLineEdit, \ - QRadioButton, QWidget +from PyQt4 import QtGui +from PyQt4.Qt import (Qt, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, + QHBoxLayout, QIcon, QLabel, QLineEdit, + QPlainTextEdit, QRadioButton, QSize, QTableWidget, QTableWidgetItem, + QVBoxLayout, QWidget) class PluginWidget(QWidget,Ui_Form): @@ -36,6 +39,7 @@ class PluginWidget(QWidget,Ui_Form): DoubleSpinBoxControls = [] LineEditControls = [] RadioButtonControls = [] + TableWidgetControls = [] for item in self.__dict__: if type(self.__dict__[item]) is QCheckBox: @@ -48,6 +52,8 @@ class PluginWidget(QWidget,Ui_Form): LineEditControls.append(str(self.__dict__[item].objectName())) elif type(self.__dict__[item]) is QRadioButton: RadioButtonControls.append(str(self.__dict__[item].objectName())) + elif type(self.__dict__[item]) is QTableWidget: + TableWidgetControls.append(str(self.__dict__[item].objectName())) option_fields = zip(CheckBoxControls, [True for i in CheckBoxControls], @@ -60,15 +66,41 @@ class PluginWidget(QWidget,Ui_Form): ['radio_button' for i in RadioButtonControls]) # LineEditControls - option_fields += zip(['exclude_genre'],['\[.+\]'],['line_edit']) + option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit']) option_fields += zip(['exclude_pattern'],[None],['line_edit']) option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit']) - option_fields += zip(['read_pattern'],['+'],['line_edit']) - option_fields += zip(['wishlist_tag'],['Wishlist'],['line_edit']) # SpinBoxControls option_fields += zip(['thumb_width'],[1.00],['spin_box']) + # Prefix rules TableWidget + # ordinal/enabled/rule name/source field/pattern/prefix + #option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'], + # [(1,True,'Read book','Tags','+','+'), + # (2,True,'Wishlist','Tags','Wishlist','x'), + # (3,False,'','','','')], + # ['table_widget','table_widget','table_widget']) + option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'], + [{'ordinal':1, + 'enabled':True, + 'name':'Read book', + 'field':'Tags', + 'pattern':'+', + 'prefix':'+'}, + {'ordinal':2, + 'enabled':True, + 'name':'Wishlist', + 'field':'Tags', + 'pattern':'Wishlist', + 'prefix':'x'}, + {'ordinal':3, + 'enabled':False, + 'name':'', + 'field':'', + 'pattern':'', + 'prefix':''}], + ['table_widget','table_widget','table_widget']) + self.OPTION_FIELDS = option_fields def initialize(self, name, db): @@ -78,23 +110,25 @@ class PluginWidget(QWidget,Ui_Form): ['generate_titles','generate_series','generate_genres', 'generate_recently_added','generate_descriptions','include_hr'] ComboBoxControls (c_type: combo_box): - ['read_source_field','exclude_source_field','header_note_source_field', + ['exclude_source_field','header_note_source_field', 'merge_source_field'] LineEditControls (c_type: line_edit): - ['exclude_genre','exclude_pattern','exclude_tags','read_pattern', - 'wishlist_tag'] + ['exclude_genre','exclude_pattern','exclude_tags'] RadioButtonControls (c_type: radio_button): ['merge_before','merge_after'] SpinBoxControls (c_type: spin_box): ['thumb_width'] + TableWidgetControls (c_type: table_widget): + ['prefix_rules_tw'] ''' - self.name = name self.db = db - self.populateComboBoxes() + self.all_custom_fields = self.db.custom_field_keys() + self.populate_combo_boxes() # Update dialog fields from stored options + prefix_rules = [] for opt in self.OPTION_FIELDS: c_name, c_def, c_type = opt opt_value = gprefs.get(self.name + '_' + c_name, c_def) @@ -114,11 +148,8 @@ class PluginWidget(QWidget,Ui_Form): getattr(self, c_name).setChecked(opt_value) elif c_type in ['spin_box']: getattr(self, c_name).setValue(float(opt_value)) - - # Init self.read_source_field_name - cs = unicode(self.read_source_field.currentText()) - read_source_spec = self.read_source_fields[cs] - self.read_source_field_name = read_source_spec['field'] + elif c_type in ['table_widget'] and c_name == 'prefix_rules_tw': + prefix_rules.append(opt_value) # Init self.exclude_source_field_name self.exclude_source_field_name = '' @@ -147,6 +178,32 @@ class PluginWidget(QWidget,Ui_Form): # Hook changes to Description section self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) + # Init prefix_rules + self.initialize_prefix_rules(prefix_rules) + self.populate_prefix_rules(prefix_rules) + + def initialize_prefix_rules(self, rules): + + # Assign icons to buttons + self.move_rule_up_tb.setToolTip('Move rule up') + self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) + self.add_rule_tb.setToolTip('Add a new rule') + self.add_rule_tb.setIcon(QIcon(I('plus.png'))) + self.delete_rule_tb.setToolTip('Delete selected rule') + self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) + self.move_rule_down_tb.setToolTip('Move rule down') + self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) + + # Configure the QTableWidget + # enabled/rule name/source field/pattern/prefix + self.prefix_rules_tw.clear() + header_labels = ['','Name','Source','Pattern','Prefix'] + self.prefix_rules_tw.setColumnCount(len(header_labels)) + self.prefix_rules_tw.setHorizontalHeaderLabels(header_labels) + self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) + self.prefix_rules_tw.setRowCount(len(rules)) + self.prefix_rules_tw.setSortingEnabled(False) + def options(self): # Save/return the current options # exclude_genre stores literally @@ -203,7 +260,7 @@ class PluginWidget(QWidget,Ui_Form): print " %s: %s" % (opt, repr(opts_dict[opt])) return opts_dict - def populateComboBoxes(self): + def populate_combo_boxes(self): # Custom column types declared in # gui2.preferences.create_custom_column:CreateCustomColumn() # As of 0.7.34: @@ -219,25 +276,9 @@ class PluginWidget(QWidget,Ui_Form): # text Column shown in the tag browser # *text Comma-separated text, like tags, shown in tag browser - all_custom_fields = self.db.custom_field_keys() - # Populate the 'Read book' hybrid - custom_fields = {} - custom_fields['Tag'] = {'field':'tag', 'datatype':u'text'} - for custom_field in all_custom_fields: - field_md = self.db.metadata_for_field(custom_field) - if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: - custom_fields[field_md['name']] = {'field':custom_field, - 'datatype':field_md['datatype']} - # Add the sorted eligible fields to the combo box - for cf in sorted(custom_fields): - self.read_source_field.addItem(cf) - self.read_source_fields = custom_fields - self.read_source_field.currentIndexChanged.connect(self.read_source_field_changed) - - # Populate the 'Excluded books' hybrid custom_fields = {} - for custom_field in all_custom_fields: + for custom_field in self.all_custom_fields: field_md = self.db.metadata_for_field(custom_field) if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: custom_fields[field_md['name']] = {'field':custom_field, @@ -253,7 +294,7 @@ class PluginWidget(QWidget,Ui_Form): # Populate the 'Header note' combo box custom_fields = {} - for custom_field in all_custom_fields: + for custom_field in self.all_custom_fields: field_md = self.db.metadata_for_field(custom_field) if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: custom_fields[field_md['name']] = {'field':custom_field, @@ -269,7 +310,7 @@ class PluginWidget(QWidget,Ui_Form): # Populate the 'Merge with Comments' combo box custom_fields = {} - for custom_field in all_custom_fields: + for custom_field in self.all_custom_fields: field_md = self.db.metadata_for_field(custom_field) if field_md['datatype'] in ['text','comments','composite']: custom_fields[field_md['name']] = {'field':custom_field, @@ -285,6 +326,42 @@ class PluginWidget(QWidget,Ui_Form): self.merge_after.setEnabled(False) self.include_hr.setEnabled(False) + def populate_prefix_rules(self,rules): + + def populate_table_row(row, data): + self.prefix_rules_tw.blockSignals(True) + self.prefix_rules_tw.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) + self.prefix_rules_tw.setItem(row, 1, QTableWidgetItem(data['name'])) + set_source_field_in_row(row, field=data['field']) + + self.prefix_rules_tw.setItem(row, 3, QTableWidgetItem(data['pattern'])) + self.prefix_rules_tw.setItem(row, 4, QTableWidgetItem(data['prefix'])) + self.prefix_rules_tw.blockSignals(False) + + def set_source_field_in_row(row, field=''): + custom_fields = {} + custom_fields['Tags'] = {'field':'tag', 'datatype':u'text'} + for custom_field in self.all_custom_fields: + field_md = self.db.metadata_for_field(custom_field) + if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: + custom_fields[field_md['name']] = {'field':custom_field, + 'datatype':field_md['datatype']} + self.prefix_rules_tw.setCellWidget(row, 2, SourceFieldComboBox(self, sorted(custom_fields.keys()), field)) + # Need to connect to something that monitors changes to control pattern field based on source_field type + + # Entry point + + for row, data in enumerate(sorted(rules, key=lambda k: k['ordinal'])): + populate_table_row(row, data) + + + + + + # Do this after populating cells + self.prefix_rules_tw.resizeColumnsToContents() + + def read_source_field_changed(self,new_index): ''' Process changes in the read_source_field combo box @@ -388,3 +465,55 @@ class PluginWidget(QWidget,Ui_Form): Process changes in the thumb_width spin box ''' pass + + +class CheckableTableWidgetItem(QTableWidgetItem): + ''' + Borrowed from kiwidude + ''' + + def __init__(self, checked=False, is_tristate=False): + QTableWidgetItem.__init__(self, '') + self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled )) + if is_tristate: + self.setFlags(self.flags() | Qt.ItemIsTristate) + if checked: + self.setCheckState(Qt.Checked) + else: + if is_tristate and checked is None: + self.setCheckState(Qt.PartiallyChecked) + else: + self.setCheckState(Qt.Unchecked) + + def get_boolean_value(self): + ''' + Return a boolean value indicating whether checkbox is checked + If this is a tristate checkbox, a partially checked value is returned as None + ''' + if self.checkState() == Qt.PartiallyChecked: + return None + else: + return self.checkState() == Qt.Checked + +class NoWheelComboBox(QComboBox): + + def wheelEvent (self, event): + # Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid + event.ignore() + +class SourceFieldComboBox(NoWheelComboBox): + + def __init__(self, parent, format_names, selected_text): + NoWheelComboBox.__init__(self, parent) + self.populate_combo(format_names, selected_text) + + def populate_combo(self, format_names, selected_text): + self.addItems(['']) + self.addItems(format_names) + if selected_text: + idx = self.findText(selected_text) + self.setCurrentIndex(idx) + else: + self.setCurrentIndex(0) + + diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 5136f86fae..31c5063a12 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -7,7 +7,7 @@ 0 0 650 - 596 + 603 @@ -203,6 +203,9 @@ e.g., [Project Gutenberg]</p> Excluded books + + QFormLayout::FieldsStayAtSizeHint + @@ -323,96 +326,66 @@ Default: ~,Catalog - + 0 0 - - - 0 - 0 - - - - Matching books will be displayed with a check mark - - Read books + Prefix rules - - - - - QLayout::SetDefaultConstraint - + + + - - - - 175 - 0 - + + + + 0 + 0 + - 200 - 16777215 + 16777215 + 118 - - &Column/value - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - read_source_field - - - - - 0 - 0 - - - - Column containing 'read' status - - - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 18 - - - - - - - - 150 - 0 - - - - 'read book' pattern - - - - - + + + + + ... + + + + + + + ... + + + + + + + ... + + + + + + + ... + + + + @@ -440,49 +413,7 @@ Default: ~,Catalog QFormLayout::FieldsStayAtSizeHint - - - - - - - 175 - 0 - - - - - 200 - 16777215 - - - - - - - &Wishlist tag - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - wishlist_tag - - - - - - - Books tagged as Wishlist items will be displayed with an X - - - - - - + @@ -542,7 +473,7 @@ Default: ~,Catalog - + @@ -599,7 +530,7 @@ Default: ~,Catalog - + diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index 81cb247236..1e9935427c 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -51,7 +51,8 @@ class EPUB_MOBI(CatalogPlugin): default=':', dest='exclude_book_marker', action = None, - help=_("field:pattern specifying custom field/contents indicating book should be excluded.\n" + help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n" + "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n" "Default: '%default'\n" "Applies to ePub, MOBI output formats")), Option('--exclude-genre', @@ -121,7 +122,7 @@ class EPUB_MOBI(CatalogPlugin): default='::', dest='merge_comments', action = None, - help=_(":[before|after]:[True|False] specifying:\n" + help=_("#:[before|after]:[True|False] specifying:\n" " Custom field containing notes to merge with Comments\n" " [before|after] Placement of notes with respect to Comments\n" " [True|False] - A horizontal rule is inserted between notes and Comments\n" @@ -134,11 +135,14 @@ class EPUB_MOBI(CatalogPlugin): help=_("Specifies the output profile. In some cases, an output profile is required to optimize the catalog for the device. For example, 'kindle' or 'kindle_dx' creates a structured Table of Contents with Sections and Articles.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), - Option('--read-book-marker', - default='tag:+', - dest='read_book_marker', - action = None, - help=_("field:pattern indicating book has been read.\n" "Default: '%default'\n" + Option('--prefix-rules', + default="(('Read books','tags','+','\u2713'),('Wishlist items','tags','Wishlist','\u00d7'))", + dest='prefix_rules', + action=None, + help=_("Specifies the rules used to include prefixes indicating read books, wishlist items and other user-specifed prefixes.\n" + "The model for a prefix rule is ('','','','').\n" + "When multiple rules are defined, the first matching rule will be used.\n" + "Default: '%default'\n" "Applies to ePub, MOBI output formats")), Option('--thumb-width', default='1.0', @@ -148,12 +152,6 @@ class EPUB_MOBI(CatalogPlugin): "Range: 1.0 - 2.0\n" "Default: '%default'\n" "Applies to ePub, MOBI output formats")), - Option('--wishlist-tag', - default='Wishlist', - dest='wishlist_tag', - action = None, - help=_("Tag indicating book to be displayed as wishlist item.\n" "Default: '%default'\n" - "Applies to: ePub, MOBI output formats")), ] # }}} @@ -276,6 +274,15 @@ class EPUB_MOBI(CatalogPlugin): log.error("coercing thumb_width from '%s' to '%s'" % (opts.thumb_width,self.THUMB_SMALLEST)) opts.thumb_width = "1.0" + # Pre-process prefix_rules + try: + opts.prefix_rules = eval(opts.prefix_rules) + except: + log.error("malformed --prefix-rules: %s" % opts.prefix_rules) + raise + for rule in opts.prefix_rules: + if len(rule) != 4: + log.error("incorrect number of args for --prefix-rules: %s" % repr(rule)) # Display opts keys = opts_dict.keys() @@ -285,7 +292,7 @@ class EPUB_MOBI(CatalogPlugin): if key in ['catalog_title','authorClip','connected_kindle','descriptionClip', 'exclude_book_marker','exclude_genre','exclude_tags', 'header_note_source_field','merge_comments', - 'output_profile','read_book_marker', + 'output_profile','prefix_rules','read_book_marker', 'search_text','sort_by','sort_descriptions_by_author','sync', 'thumb_width','wishlist_tag']: build_log.append(" %s: %s" % (key, repr(opts_dict[key]))) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 76f96a3397..949b1a6b81 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -72,6 +72,7 @@ class CatalogBuilder(object): self.__currentStep = 0.0 self.__creator = opts.creator self.__db = db + self.__defaultPrefix = None self.__descriptionClip = opts.descriptionClip self.__error = [] self.__generateForKindle = True if (self.opts.fmt == 'mobi' and \ @@ -91,10 +92,9 @@ class CatalogBuilder(object): self.__output_profile = None self.__playOrder = 1 self.__plugin = plugin + self.__prefixRules = [] self.__progressInt = 0.0 self.__progressString = '' - f, _, p = opts.read_book_marker.partition(':') - self.__read_book_marker = {'field':f, 'pattern':p} f, p, hr = self.opts.merge_comments.split(':') self.__merge_comments = {'field':f, 'position':p, 'hr':hr} self.__reporter = report_progress @@ -113,6 +113,9 @@ class CatalogBuilder(object): self.__output_profile = profile break + # Process prefix rules + self.processPrefixRules() + # Confirm/create thumbs archive. if self.opts.generate_descriptions: if not os.path.exists(self.__cache_dir): @@ -269,6 +272,13 @@ class CatalogBuilder(object): return self.__db return property(fget=fget) @dynamic_property + def defaultPrefix(self): + def fget(self): + return self.__defaultPrefix + def fset(self, val): + self.__defaultPrefix = val + return property(fget=fget, fset=fset) + @dynamic_property def descriptionClip(self): def fget(self): return self.__descriptionClip @@ -363,6 +373,13 @@ class CatalogBuilder(object): return self.__plugin return property(fget=fget) @dynamic_property + def prefixRules(self): + def fget(self): + return self.__prefixRules + def fset(self, val): + self.__prefixRules = val + return property(fget=fget, fset=fset) + @dynamic_property def progressInt(self): def fget(self): return self.__progressInt @@ -437,27 +454,12 @@ class CatalogBuilder(object): return property(fget=fget, fset=fset) @dynamic_property - def MISSING_SYMBOL(self): - def fget(self): - return self.__output_profile.missing_char - return property(fget=fget) - @dynamic_property - def NOT_READ_SYMBOL(self): - def fget(self): - return '%s' % self.__output_profile.read_char - return property(fget=fget) - @dynamic_property def READING_SYMBOL(self): def fget(self): return '' if self.generateForKindle else \ '+' return property(fget=fget) @dynamic_property - def READ_SYMBOL(self): - def fget(self): - return self.__output_profile.read_char - return property(fget=fget) - @dynamic_property def FULL_RATING_SYMBOL(self): def fget(self): return self.__output_profile.ratings_char @@ -750,7 +752,7 @@ Author '{0}': if record['cover']: this_title['cover'] = re.sub('&', '&', record['cover']) - this_title['read'] = self.discoverReadStatus(record) + this_title['prefix'] = self.discoverPrefix(record) if record['tags']: this_title['tags'] = self.processSpecialTags(record['tags'], @@ -991,28 +993,16 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in book.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if book['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif book['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 + # Link to book aTag = Tag(soup, "a") @@ -1026,12 +1016,12 @@ Author '{0}': else: formatted_title = self.by_titles_normal_title_template.format(**args).rstrip() aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) - ptc += 1 + spanTag.insert(stc, aTag) + stc += 1 # Dot - pBookTag.insert(ptc, NavigableString(" · ")) - ptc += 1 + spanTag.insert(stc, NavigableString(" · ")) + stc += 1 # Link to author emTag = Tag(soup, "em") @@ -1040,7 +1030,10 @@ Author '{0}': aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) aTag.insert(0, NavigableString(book['author'])) emTag.insert(0,aTag) - pBookTag.insert(ptc, emTag) + spanTag.insert(stc, emTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 if divRunningTag is not None: @@ -1172,10 +1165,8 @@ Author '{0}': aTag['href'] = "%s.html#%s_series" % ('BySeries', re.sub('\W','',book['series']).lower()) aTag.insert(0, book['series']) - #pSeriesTag.insert(0, NavigableString(self.NOT_READ_SYMBOL)) pSeriesTag.insert(0, aTag) else: - #pSeriesTag.insert(0,NavigableString(self.NOT_READ_SYMBOL + '%s' % book['series'])) pSeriesTag.insert(0,NavigableString('%s' % book['series'])) if author_count == 1: @@ -1189,28 +1180,15 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in book.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if book['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif book['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 aTag = Tag(soup, "a") if self.opts.generate_descriptions: @@ -1227,7 +1205,9 @@ Author '{0}': non_series_books += 1 aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) + spanTag.insert(ptc, aTag) + stc += 1 + pBookTag.insert(ptc, spanTag) ptc += 1 if author_count == 1: @@ -1337,28 +1317,15 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in new_entry.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if new_entry['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif new_entry['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 aTag = Tag(soup, "a") if self.opts.generate_descriptions: @@ -1372,7 +1339,10 @@ Author '{0}': formatted_title = self.by_month_added_normal_title_template.format(**args).rstrip() non_series_books += 1 aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) + spanTag.insert(stc, aTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 divTag.insert(dtc, pBookTag) @@ -1393,28 +1363,15 @@ Author '{0}': for new_entry in date_range_list: # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in new_entry.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if new_entry['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif new_entry['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(new_entry['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 aTag = Tag(soup, "a") if self.opts.generate_descriptions: @@ -1427,12 +1384,12 @@ Author '{0}': else: formatted_title = self.by_recently_added_normal_title_template.format(**args).rstrip() aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) - ptc += 1 + spanTag.insert(stc, aTag) + stc += 1 # Dot - pBookTag.insert(ptc, NavigableString(" · ")) - ptc += 1 + spanTag.insert(stc, NavigableString(" · ")) + stc += 1 # Link to author emTag = Tag(soup, "em") @@ -1441,7 +1398,10 @@ Author '{0}': aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author'])) aTag.insert(0, NavigableString(new_entry['author'])) emTag.insert(0,aTag) - pBookTag.insert(ptc, emTag) + spanTag.insert(stc, emTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 divTag.insert(dtc, pBookTag) @@ -1799,30 +1759,16 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - book['read'] = self.discoverReadStatus(book) + book['prefix'] = self.discoverPrefix(book) + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in book.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if book.get('read', False): - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif book['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 aTag = Tag(soup, "a") if self.opts.generate_descriptions: @@ -1838,12 +1784,13 @@ Author '{0}': args = self.generateFormatArgs(book) formatted_title = self.by_series_title_template.format(**args).rstrip() aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) - ptc += 1 + + spanTag.insert(stc, aTag) + stc += 1 # · - pBookTag.insert(ptc, NavigableString(' · ')) - ptc += 1 + spanTag.insert(stc, NavigableString(' · ')) + stc += 1 # Link to author aTag = Tag(soup, "a") @@ -1851,7 +1798,10 @@ Author '{0}': aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(escape(' & '.join(book['authors'])))) aTag.insert(0, NavigableString(' & '.join(book['authors']))) - pBookTag.insert(ptc, aTag) + spanTag.insert(stc, aTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 divTag.insert(dtc, pBookTag) @@ -1905,7 +1855,7 @@ Author '{0}': this_book['author'] = book['author'] this_book['title'] = book['title'] this_book['author_sort'] = capitalize(book['author_sort']) - this_book['read'] = book['read'] + this_book['prefix'] = book['prefix'] this_book['tags'] = book['tags'] this_book['id'] = book['id'] this_book['series'] = book['series'] @@ -3165,7 +3115,7 @@ Author '{0}': if not os.path.isdir(images_path): os.makedirs(images_path) - def discoverReadStatus(self, record): + def discoverPrefix(self, record): ''' Given a field:pattern spec, discover if this book marked as read @@ -3177,25 +3127,42 @@ Author '{0}': datatype datetime: #field_name:.* ''' - # Legacy handling of special 'read' tag - field = self.__read_book_marker['field'] - pat = self.__read_book_marker['pattern'] - if field == 'tag' and pat in record['tags']: - return True + if self.opts.verbose: + self.opts.log.info("\tevaluating %s (%s) for prefix matches" % (record['title'], record['authors'])) + # Compare the record to each rule looking for a match + for rule in self.prefixRules: + if False and self.opts.verbose: + self.opts.log.info("\t evaluating prefix_rule '%s'" % rule['name']) - field_contents = self.__db.get_field(record['id'], - field, + # Literal comparison for Tags field + if rule['field'].lower() == 'tags': + if rule['pattern'].lower() in map(unicode.lower,record['tags']): + if self.opts.verbose: + self.opts.log.info("\t '%s' found in '%s' (%s)" % + (rule['pattern'], rule['field'], rule['name'])) + return rule['prefix'] + + # Regex comparison for custom field + elif rule['field'].startswith('#'): + field_contents = self.__db.get_field(record['id'], + rule['field'], index_is_id=True) - if field_contents: - try: - if re.search(pat, unicode(field_contents), - re.IGNORECASE) is not None: - return True - except: - # Compiling of pat failed, ignore it - pass + if field_contents: + try: + if re.search(rule['pattern'], unicode(field_contents), + re.IGNORECASE) is not None: + if self.opts.verbose: + self.opts.log.info("\t '%s' found in '%s' (%s)" % + (rule['pattern'], rule['field'], rule['name'])) + return rule['prefix'] + except: + # Compiling of pat failed, ignore it + pass + + if False and self.opts.verbose: + self.opts.log.info("\t No prefix match found") + return None - return False def filterDbTags(self, tags): # Remove the special marker tags from the database's tag list, @@ -3227,9 +3194,13 @@ Author '{0}': if tag in self.markerTags: excluded_tags.append(tag) continue - if re.search(self.opts.exclude_genre, tag): - excluded_tags.append(tag) - continue + try: + if re.search(self.opts.exclude_genre, tag): + excluded_tags.append(tag) + continue + except: + self.opts.log.error("\tfilterDbTags(): malformed --exclude-genre regex pattern: %s" % self.opts.exclude_genre) + if tag == ' ': continue @@ -3266,6 +3237,20 @@ Author '{0}': else: return None + def formatPrefix(self,prefix_char,soup): + # Generate the HTML for the prefix portion of the listing + spanTag = Tag(soup, "span") + if prefix_char is None: + spanTag['style'] = "color:white" + spanTag.insert(0,NavigableString(self.defaultPrefix)) + # 2e3a is 'two-em dash', which matches width in Kindle Previewer + # too wide in calibre viewer + # minimal visual distraction + # spanTag.insert(0,NavigableString(u'\u2e3a')) + else: + spanTag.insert(0,NavigableString(prefix_char)) + return spanTag + def generateAuthorAnchor(self, author): # Strip white space to '' return re.sub("\W","", author) @@ -3359,28 +3344,15 @@ Author '{0}': # Add books pBookTag = Tag(soup, "p") + pBookTag['class'] = "line_item" ptc = 0 - # book with read|reading|unread symbol or wishlist item - if self.opts.wishlist_tag in book.get('tags', []): - pBookTag['class'] = "wishlist_item" - pBookTag.insert(ptc,NavigableString(self.MISSING_SYMBOL)) - ptc += 1 - else: - if book['read']: - # check mark - pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - elif book['id'] in self.bookmarked_books: - pBookTag.insert(ptc,NavigableString(self.READING_SYMBOL)) - pBookTag['class'] = "read_book" - ptc += 1 - else: - # hidden check mark - pBookTag['class'] = "unread_book" - pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) - ptc += 1 + pBookTag.insert(ptc, self.formatPrefix(book['prefix'],soup)) + ptc += 1 + + spanTag = Tag(soup, "span") + spanTag['class'] = "entry" + stc = 0 # Add the book title aTag = Tag(soup, "a") @@ -3398,7 +3370,10 @@ Author '{0}': non_series_books += 1 aTag.insert(0,NavigableString(escape(formatted_title))) - pBookTag.insert(ptc, aTag) + spanTag.insert(stc, aTag) + stc += 1 + + pBookTag.insert(ptc, spanTag) ptc += 1 divTag.insert(dtc, pBookTag) @@ -3463,15 +3438,13 @@ Author '{0}': # Author, author_prefix (read|reading|none symbol or missing symbol) author = book['author'] - if self.opts.wishlist_tag in book.get('tags', []): - author_prefix = self.MISSING_SYMBOL + " by " + + if book['prefix']: + author_prefix = book['prefix'] + " by " + elif self.opts.connected_kindle and book['id'] in self.bookmarked_books: + author_prefix = self.READING_SYMBOL + " by " else: - if book['read']: - author_prefix = self.READ_SYMBOL + " by " - elif self.opts.connected_kindle and book['id'] in self.bookmarked_books: - author_prefix = self.READING_SYMBOL + " by " - else: - author_prefix = "by " + author_prefix = "by " # Genres genres = '' @@ -4005,6 +3978,22 @@ Author '{0}': return merged + def processPrefixRules(self): + # Put the prefix rules into an ordered list of dicts + try: + for rule in self.opts.prefix_rules: + prefix_rule = {} + prefix_rule['name'] = rule[0] + prefix_rule['field'] = rule[1] + prefix_rule['pattern'] = rule[2] + prefix_rule['prefix'] = rule[3] + self.prefixRules.append(prefix_rule) + except: + self.opts.log.error("malformed self.opts.prefix_rules: %s" % repr(self.opts.prefix_rules)) + raise + # Use the highest order prefix symbol as default + self.defaultPrefix = self.opts.prefix_rules[0][3] + def processExclusions(self, data_set): ''' Remove excluded entries @@ -4026,17 +4015,20 @@ Author '{0}': return filtered_data_set def processSpecialTags(self, tags, this_title, opts): + tag_list = [] - for tag in tags: - tag = self.convertHTMLEntities(tag) - if re.search(opts.exclude_genre, tag): - continue - elif self.__read_book_marker['field'] == 'tag' and \ - tag == self.__read_book_marker['pattern']: - # remove 'read' tag - continue - else: - tag_list.append(tag) + + try: + for tag in tags: + tag = self.convertHTMLEntities(tag) + if re.search(opts.exclude_genre, tag): + continue + else: + tag_list.append(tag) + except: + self.opts.log.error("\tprocessSpecialTags(): malformed --exclude-genre regex pattern: %s" % opts.exclude_genre) + return tags + return tag_list def updateProgressFullStep(self, description): diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 4fc268ce7d..2b65ed37da 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -719,6 +719,7 @@ def catalog_option_parser(args): def add_plugin_parser_options(fmt, parser, log): # Fetch the extension-specific CLI options from the plugin + # library.catalogs..py plugin = plugin_for_catalog_format(fmt) for option in plugin.cli_options: if option.action: @@ -796,7 +797,6 @@ def catalog_option_parser(args): return parser, plugin, log def command_catalog(args, dbpath): - print("library.cli:command_catalog() EXPERIMENTAL MODE") parser, plugin, log = catalog_option_parser(args) opts, args = parser.parse_args(sys.argv[1:]) From 67c98bae56fdfad46aca98a3a002b8ec896bd384 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sun, 5 Aug 2012 04:38:35 -0600 Subject: [PATCH 03/11] Stable version with prefix_rules* functions --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 460 +++++++++--------- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 12 +- .../library/catalogs/epub_mobi_builder.py | 39 +- 3 files changed, 256 insertions(+), 255 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 7f76406718..54e8bf5db9 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -112,131 +112,6 @@ class PluginWidget(QWidget,Ui_Form): 'datatype':field_md['datatype']} self.eligible_custom_fields = custom_fields - def generatePrefixList(self): - def prefix_sorter(item): - key = item - if item[0] == "_": - key = 'zzz' + item - return key - - - # Create a list of prefixes for user selection - raw_prefix_list = [ - ('Ampersand',u'&'), - ('Angle left double',u'\u00ab'), - ('Angle left',u'\u2039'), - ('Angle right double',u'\u00bb'), - ('Angle right',u'\u203a'), - ('Arrow double',u'\u2194'), - ('Arrow down',u'\u2193'), - ('Arrow left',u'\u2190'), - ('Arrow right',u'\u2192'), - ('Arrow up',u'\u2191'), - ('Asterisk',u'*'), - ('At sign',u'@'), - ('Bullet smallest',u'\u22c5'), - ('Bullet small',u'\u00b7'), - ('Bullet',u'\u2022'), - ('Caret',u'^'), - ('Checkmark',u'\u2713'), - ('Copyright',u'\u00a9'), - ('Currency dollar',u'$'), - ('Currency euro',u'\u20ac'), - ('Dagger double',u'\u2021'), - ('Dagger',u'\u2020'), - ('Degree',u'\u00b0'), - ('Dots3',u'\u2234'), - ('Hash',u'#'), - ('Infinity',u'\u221e'), - ('Lozenge',u'\u25ca'), - ('Math divide',u'\u00f7'), - ('Math empty',u'\u2205'), - ('Math equals',u'='), - ('Math minus',u'\u2212'), - ('Math plus circled',u'\u2295'), - ('Math times circled',u'\u2297'), - ('Math times',u'\u00d7'), - ('O slash',u'\u00d8'), - ('Paragraph',u'\u00b6'), - ('Percent',u'%'), - ('Plus-or-minus',u'\u00b1'), - ('Plus',u'+'), - ('Punctuation colon',u':'), - ('Punctuation colon-semi',u';'), - ('Punctuation exclamation',u'!'), - ('Punctuation question',u'?'), - ('Registered trademark',u'\u00ae'), - ('Section',u'\u00a7'), - ('Tilde',u'~'), - ('Vertical bar',u'|'), - ('Vertical bar broken',u'\u00a6'), - ('_0',u'0'), - ('_1',u'1'), - ('_2',u'2'), - ('_3',u'3'), - ('_4',u'4'), - ('_5',u'5'), - ('_6',u'6'), - ('_7',u'7'), - ('_8',u'8'), - ('_9',u'9'), - ('_A',u'A'), - ('_B',u'B'), - ('_C',u'C'), - ('_D',u'D'), - ('_E',u'E'), - ('_F',u'F'), - ('_G',u'G'), - ('_H',u'H'), - ('_I',u'I'), - ('_J',u'J'), - ('_K',u'K'), - ('_L',u'L'), - ('_M',u'M'), - ('_N',u'N'), - ('_O',u'O'), - ('_P',u'P'), - ('_Q',u'Q'), - ('_R',u'R'), - ('_S',u'S'), - ('_T',u'T'), - ('_U',u'U'), - ('_V',u'V'), - ('_W',u'W'), - ('_X',u'X'), - ('_Y',u'Y'), - ('_Z',u'Z'), - ('_a',u'a'), - ('_b',u'b'), - ('_c',u'c'), - ('_d',u'd'), - ('_e',u'e'), - ('_f',u'f'), - ('_g',u'g'), - ('_h',u'h'), - ('_i',u'i'), - ('_j',u'j'), - ('_k',u'k'), - ('_l',u'l'), - ('_m',u'm'), - ('_n',u'n'), - ('_o',u'o'), - ('_p',u'p'), - ('_q',u'q'), - ('_r',u'r'), - ('_s',u's'), - ('_t',u't'), - ('_u',u'u'), - ('_v',u'v'), - ('_w',u'w'), - ('_x',u'x'), - ('_y',u'y'), - ('_z',u'z'), - ] - #raw_prefix_list = sorted(raw_prefix_list, key=lambda k: sort_key(k[0])) - raw_prefix_list = sorted(raw_prefix_list, key=prefix_sorter) - self.prefixes = [x[1] for x in raw_prefix_list] - def initialize(self, name, db): ''' @@ -259,7 +134,6 @@ class PluginWidget(QWidget,Ui_Form): self.name = name self.db = db self.fetchEligibleCustomFields() - self.generatePrefixList() self.populate_combo_boxes() @@ -315,41 +189,8 @@ class PluginWidget(QWidget,Ui_Form): # Hook changes to Description section self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) - # Neaten up the prefix rules - self.prefix_rules_initialize() - self.populate_prefix_rules(prefix_rules) - self.prefix_rules_tw.resizeColumnsToContents() - self.prefix_rules_resize_name(1.5) - self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) - - def prefix_rules_initialize(self): - # Assign icons to buttons, hook clicks - self.move_rule_up_tb.setToolTip('Move rule up') - self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) - self.move_rule_up_tb.clicked.connect(self.prefix_rules_move_row_up) - - self.add_rule_tb.setToolTip('Add a new rule') - self.add_rule_tb.setIcon(QIcon(I('plus.png'))) - self.add_rule_tb.clicked.connect(self.prefix_rules_add_row) - - self.delete_rule_tb.setToolTip('Delete selected rule') - self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) - self.delete_rule_tb.clicked.connect(self.prefix_rules_delete_row) - - self.move_rule_down_tb.setToolTip('Move rule down') - self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) - self.move_rule_down_tb.clicked.connect(self.prefix_rules_move_row_down) - - # Configure the QTableWidget - # enabled/rule name/source field/pattern/prefix - self.prefix_rules_tw.clear() - header_labels = ['','Name','Prefix','Source','Pattern'] - self.prefix_rules_tw.setColumnCount(len(header_labels)) - self.prefix_rules_tw.setHorizontalHeaderLabels(header_labels) - self.prefix_rules_tw.setSortingEnabled(False) - self.prefix_rules_tw.setSelectionBehavior(QAbstractItemView.SelectRows) - - self.prefix_rules_tw.cellDoubleClicked.connect(self.prefix_rules_cell_double_clicked) + # Initialize prefix rules + self.prefix_rules_initialize(prefix_rules) def options(self): # Save/return the current options @@ -505,75 +346,18 @@ class PluginWidget(QWidget,Ui_Form): self.merge_after.setEnabled(False) self.include_hr.setEnabled(False) - def populate_prefix_rules(self,rules): - # Format of rules list is different if default values vs retrieved JSON - # Hack to normalize list style - if type(rules[0]) is list: - rules = rules[0] - self.prefix_rules_tw.setFocus() - for row, rule in enumerate(rules): - self.prefix_rules_tw.insertRow(row) - self.prefix_rules_select_and_scroll_to_row(row) - self.populate_prefix_rules_table_row(row, rule) - self.prefix_rules_tw.selectRow(0) - - def populate_prefix_rules_table_row(self, row, data): - - def set_prefix_field_in_row(row, col, field=''): - prefix_combo = PrefixRulesComboBox(self, self.prefixes, field) - self.prefix_rules_tw.setCellWidget(row, col, prefix_combo) - - def set_rule_name_in_row(row, col, name=''): - rule_name = QLineEdit(name) - rule_name.home(False) - rule_name.editingFinished.connect(self.prefix_rules_rule_name_edited) - self.prefix_rules_tw.setCellWidget(row, col, rule_name) - - def set_source_field_in_row(row, col, field=''): - source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) - source_combo.currentIndexChanged.connect(partial(self.prefix_rules_source_index_changed, source_combo, row)) - #source_combo.currentIndexChanged.connect(self.prefix_rules_source_index_changed, source_combo, row) - self.prefix_rules_tw.setCellWidget(row, col, source_combo) - return source_combo - - - # Entry point - self.prefix_rules_tw.blockSignals(True) - #print("populate_prefix_rules_table_row processing rule:\n%s\n" % data) - - # Column 0: Enabled - self.prefix_rules_tw.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) - - # Column 1: Rule name - #rule_name = QTableWidgetItem(data['name']) - #self.prefix_rules_tw.setItem(row, 1, rule_name) - set_rule_name_in_row(row, 1, name=data['name']) - - # Column 2: Prefix - set_prefix_field_in_row(row, 2, field=data['prefix']) - - # Column 3: Source field - source_combo = set_source_field_in_row(row, 3, field=data['field']) - - # Column 4: Pattern - # The contents of the Pattern field is driven by the Source field - self.prefix_rules_source_index_changed(source_combo, row, 4, pattern=data['pattern']) - - self.prefix_rules_tw.blockSignals(False) def prefix_rules_add_row(self): # Called when '+' clicked self.prefix_rules_tw.setFocus() row = self.prefix_rules_tw.currentRow() + 1 self.prefix_rules_tw.insertRow(row) - self.populate_prefix_rules_table_row(row, self.prefix_rules_create_blank_row_data()) + self.prefix_rules_populate_table_row(row, self.prefix_rules_create_blank_row_data()) self.prefix_rules_select_and_scroll_to_row(row) self.prefix_rules_tw.resizeColumnsToContents() + # Just in case table was empty self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) - def prefix_rules_cell_double_clicked(self, row, col): - print("prefix_rules_cell_double_clicked: row:%d col:%d" % (row, col)) - def prefix_rules_convert_row_to_data(self, row): data = self.prefix_rules_create_blank_row_data() data['ordinal'] = row @@ -612,6 +396,142 @@ class PluginWidget(QWidget,Ui_Form): elif self.prefix_rules_tw.rowCount() > 0: self.prefix_rules_select_and_scroll_to_row(first_sel_row - 1) + def prefix_rules_generate_prefix_list(self): + def prefix_sorter(item): + key = item + if item[0] == "_": + key = 'zzz' + item + return key + + + # Create a list of prefixes for user selection + raw_prefix_list = [ + ('Ampersand',u'&'), + ('Angle left double',u'\u00ab'), + ('Angle left',u'\u2039'), + ('Angle right double',u'\u00bb'), + ('Angle right',u'\u203a'), + ('Arrow carriage return',u'\u21b5'), + ('Arrow double',u'\u2194'), + ('Arrow down',u'\u2193'), + ('Arrow left',u'\u2190'), + ('Arrow right',u'\u2192'), + ('Arrow up',u'\u2191'), + ('Asterisk',u'*'), + ('At sign',u'@'), + ('Bullet smallest',u'\u22c5'), + ('Bullet small',u'\u00b7'), + ('Bullet',u'\u2022'), + ('Cards clubs',u'\u2663'), + ('Cards diamonds',u'\u2666'), + ('Cards hearts',u'\u2665'), + ('Cards spades',u'\u2660'), + ('Caret',u'^'), + ('Checkmark',u'\u2713'), + ('Copyright circle c',u'\u00a9'), + ('Copyright circle r',u'\u00ae'), + ('Copyright trademark',u'\u2122'), + ('Currency cent',u'\u00a2'), + ('Currency dollar',u'$'), + ('Currency euro',u'\u20ac'), + ('Currency pound',u'\u00a3'), + ('Currency yen',u'\u00a5'), + ('Dagger double',u'\u2021'), + ('Dagger',u'\u2020'), + ('Degree',u'\u00b0'), + ('Dots3',u'\u2234'), + ('Hash',u'#'), + ('Infinity',u'\u221e'), + ('Lozenge',u'\u25ca'), + ('Math divide',u'\u00f7'), + ('Math empty',u'\u2205'), + ('Math equals',u'='), + ('Math minus',u'\u2212'), + ('Math plus circled',u'\u2295'), + ('Math times circled',u'\u2297'), + ('Math times',u'\u00d7'), + ('O slash',u'\u00d8'), + ('Paragraph',u'\u00b6'), + ('Percent',u'%'), + ('Plus-or-minus',u'\u00b1'), + ('Plus',u'+'), + ('Punctuation colon',u':'), + ('Punctuation colon-semi',u';'), + ('Punctuation exclamation',u'!'), + ('Punctuation question',u'?'), + ('Punctuation period',u'.'), + ('Punctuation slash back',u'\\'), + ('Punctuation slash forward',u'/'), + ('Section',u'\u00a7'), + ('Tilde',u'~'), + ('Vertical bar',u'|'), + ('Vertical bar broken',u'\u00a6'), + ('_0',u'0'), + ('_1',u'1'), + ('_2',u'2'), + ('_3',u'3'), + ('_4',u'4'), + ('_5',u'5'), + ('_6',u'6'), + ('_7',u'7'), + ('_8',u'8'), + ('_9',u'9'), + ('_A',u'A'), + ('_B',u'B'), + ('_C',u'C'), + ('_D',u'D'), + ('_E',u'E'), + ('_F',u'F'), + ('_G',u'G'), + ('_H',u'H'), + ('_I',u'I'), + ('_J',u'J'), + ('_K',u'K'), + ('_L',u'L'), + ('_M',u'M'), + ('_N',u'N'), + ('_O',u'O'), + ('_P',u'P'), + ('_Q',u'Q'), + ('_R',u'R'), + ('_S',u'S'), + ('_T',u'T'), + ('_U',u'U'), + ('_V',u'V'), + ('_W',u'W'), + ('_X',u'X'), + ('_Y',u'Y'), + ('_Z',u'Z'), + ('_a',u'a'), + ('_b',u'b'), + ('_c',u'c'), + ('_d',u'd'), + ('_e',u'e'), + ('_f',u'f'), + ('_g',u'g'), + ('_h',u'h'), + ('_i',u'i'), + ('_j',u'j'), + ('_k',u'k'), + ('_l',u'l'), + ('_m',u'm'), + ('_n',u'n'), + ('_o',u'o'), + ('_p',u'p'), + ('_q',u'q'), + ('_r',u'r'), + ('_s',u's'), + ('_t',u't'), + ('_u',u'u'), + ('_v',u'v'), + ('_w',u'w'), + ('_x',u'x'), + ('_y',u'y'), + ('_z',u'z'), + ] + raw_prefix_list = sorted(raw_prefix_list, key=prefix_sorter) + self.prefixes = [x[1] for x in raw_prefix_list] + def prefix_rules_get_data(self): data_items = [] for row in range(self.prefix_rules_tw.rowCount()): @@ -625,6 +545,42 @@ class PluginWidget(QWidget,Ui_Form): 'prefix':data['prefix']}) return data_items + def prefix_rules_initialize(self, prefix_rules): + # Assign icons to buttons, hook clicks + self.move_rule_up_tb.setToolTip('Move rule up') + self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) + self.move_rule_up_tb.clicked.connect(self.prefix_rules_move_row_up) + + self.add_rule_tb.setToolTip('Add a new rule') + self.add_rule_tb.setIcon(QIcon(I('plus.png'))) + self.add_rule_tb.clicked.connect(self.prefix_rules_add_row) + + self.delete_rule_tb.setToolTip('Delete selected rule') + self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) + self.delete_rule_tb.clicked.connect(self.prefix_rules_delete_row) + + self.move_rule_down_tb.setToolTip('Move rule down') + self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) + self.move_rule_down_tb.clicked.connect(self.prefix_rules_move_row_down) + + # Configure the QTableWidget + # enabled/prefix/rule name/source field/pattern + self.prefix_rules_tw.clear() + header_labels = ['','Name','Prefix','Source','Pattern'] + self.prefix_rules_tw.setColumnCount(len(header_labels)) + self.prefix_rules_tw.setHorizontalHeaderLabels(header_labels) + self.prefix_rules_tw.setSortingEnabled(False) + self.prefix_rules_tw.setSelectionBehavior(QAbstractItemView.SelectRows) + + # Generate the prefix list + self.prefix_rules_generate_prefix_list() + + # Populate the table + self.prefix_rules_populate(prefix_rules) + self.prefix_rules_tw.resizeColumnsToContents() + self.prefix_rules_resize_name(1.5) + self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) + def prefix_rules_move_row_down(self): self.prefix_rules_tw.setFocus() rows = self.prefix_rules_tw.selectionModel().selectedRows() @@ -649,7 +605,7 @@ class PluginWidget(QWidget,Ui_Form): self.prefix_rules_tw.insertRow(src_row) # Populate it with the saved data - self.populate_prefix_rules_table_row(src_row, saved_data) + self.prefix_rules_populate_table_row(src_row, saved_data) self.blockSignals(False) scroll_to_row = last_sel_row + 1 if scroll_to_row < self.prefix_rules_tw.rowCount() - 1: @@ -671,7 +627,7 @@ class PluginWidget(QWidget,Ui_Form): # Add a row below us with the source data self.prefix_rules_tw.insertRow(selrow.row() + 1) - self.populate_prefix_rules_table_row(selrow.row() + 1, saved_data) + self.prefix_rules_populate_table_row(selrow.row() + 1, saved_data) # Delete the row above self.prefix_rules_tw.removeRow(selrow.row() - 1) @@ -682,6 +638,63 @@ class PluginWidget(QWidget,Ui_Form): scroll_to_row = scroll_to_row - 1 self.prefix_rules_tw.scrollToItem(self.prefix_rules_tw.item(scroll_to_row, 0)) + def prefix_rules_populate(self,rules): + # Format of rules list is different if default values vs retrieved JSON + # Hack to normalize list style + if type(rules[0]) is list: + rules = rules[0] + self.prefix_rules_tw.setFocus() + rules = sorted(rules, key=lambda k: k['ordinal']) + for row, rule in enumerate(rules): + self.prefix_rules_tw.insertRow(row) + self.prefix_rules_select_and_scroll_to_row(row) + self.prefix_rules_populate_table_row(row, rule) + self.prefix_rules_tw.selectRow(0) + + def prefix_rules_populate_table_row(self, row, data): + + def set_prefix_field_in_row(row, col, field=''): + prefix_combo = PrefixRulesComboBox(self, self.prefixes, field) + self.prefix_rules_tw.setCellWidget(row, col, prefix_combo) + + def set_rule_name_in_row(row, col, name=''): + rule_name = QLineEdit(name) + rule_name.home(False) + rule_name.editingFinished.connect(self.prefix_rules_rule_name_edited) + self.prefix_rules_tw.setCellWidget(row, col, rule_name) + + def set_source_field_in_row(row, col, field=''): + source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) + source_combo.currentIndexChanged.connect(partial(self.prefix_rules_source_index_changed, source_combo, row)) + #source_combo.currentIndexChanged.connect(self.prefix_rules_source_index_changed, source_combo, row) + self.prefix_rules_tw.setCellWidget(row, col, source_combo) + return source_combo + + + # Entry point + self.prefix_rules_tw.blockSignals(True) + #print("prefix_rules_populate_table_row processing rule:\n%s\n" % data) + + # Column 0: Enabled + self.prefix_rules_tw.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) + + # Column 1: Rule name + #rule_name = QTableWidgetItem(data['name']) + #self.prefix_rules_tw.setItem(row, 1, rule_name) + set_rule_name_in_row(row, 1, name=data['name']) + + # Column 2: Prefix + set_prefix_field_in_row(row, 2, field=data['prefix']) + + # Column 3: Source field + source_combo = set_source_field_in_row(row, 3, field=data['field']) + + # Column 4: Pattern + # The contents of the Pattern field is driven by the Source field + self.prefix_rules_source_index_changed(source_combo, row, 4, pattern=data['pattern']) + + self.prefix_rules_tw.blockSignals(False) + def prefix_rules_resize_name(self, scale): current_width = self.prefix_rules_tw.columnWidth(1) self.prefix_rules_tw.setColumnWidth(1, min(225,int(current_width * scale))) @@ -719,6 +732,7 @@ class PluginWidget(QWidget,Ui_Form): values_combo = PrefixRulesComboBox(self, values, pattern) self.prefix_rules_tw.setCellWidget(row, 4, values_combo) + def read_source_field_changed(self,new_index): ''' Process changes in the read_source_field combo box @@ -873,5 +887,3 @@ class PrefixRulesComboBox(NoWheelComboBox): self.setCurrentIndex(idx) else: self.setCurrentIndex(0) - - diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 31c5063a12..eb39415a81 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -107,10 +107,11 @@ - <p>Default pattern -\[.+\] -excludes tags of the form [tag], -e.g., [Project Gutenberg]</p> + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html> Excluded genres @@ -333,6 +334,9 @@ Default: ~,Catalog 0 + + The first matching rule will be used to add a prefix to book listings in the generated catalog. + Prefix rules diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index edb9567d93..6cc94c0e08 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -3117,32 +3117,24 @@ Author '{0}': def discoverPrefix(self, record): ''' - Given a field:pattern spec, discover if this book marked as read - - if field == tag, scan tags for pattern - if custom field, try regex match for pattern - This allows maximum flexibility with fields of type - datatype bool: #field_name:True - datatype text: #field_name: - datatype datetime: #field_name:.* - + Evaluate conditions for including prefixes in various listings ''' - if self.opts.verbose: - self.opts.log.info("\tevaluating %s (%s)" % (record['title'], record['authors'])) + def log_prefix_rule_match_info(rule, record): + self.opts.log.info(" %s %s by %s (Prefix rule '%s': %s:%s)" % + (rule['prefix'],record['title'], + record['authors'][0], rule['name'], + rule['field'],rule['pattern'])) + # Compare the record to each rule looking for a match for rule in self.prefixRules: - if self.opts.verbose: - self.opts.log.info("\t prefix_rule '%s': %s" % (rule['name'], rule)) - # Literal comparison for Tags field if rule['field'].lower() == 'tags': if rule['pattern'].lower() in map(unicode.lower,record['tags']): if self.opts.verbose: - self.opts.log.info("\t '%s' found in '%s' (%s)" % - (rule['pattern'], rule['field'], rule['name'])) + log_prefix_rule_match_info(rule, record) return rule['prefix'] - # Regex comparison for custom field + # Regex match for custom field elif rule['field'].startswith('#'): field_contents = self.__db.get_field(record['id'], rule['field'], @@ -3150,30 +3142,23 @@ Author '{0}': if field_contents == '': field_contents = None - if self.opts.verbose: - self.opts.log.info("\t field_contents: %s" % repr(field_contents)) - if field_contents is not None: try: if re.search(rule['pattern'], unicode(field_contents), re.IGNORECASE) is not None: if self.opts.verbose: - self.opts.log.info("\t '%s' matches '%s' value (%s)" % - (rule['pattern'], rule['field'], rule['name'])) + log_prefix_rule_match_info(rule, record) return rule['prefix'] except: # Compiling of pat failed, ignore it if self.opts.verbose: - self.opts.log.info("pattern failed to compile: %s" % rule['pattern']) + self.opts.log.error("pattern failed to compile: %s" % rule['pattern']) pass elif field_contents is None and rule['pattern'] == 'None': if self.opts.verbose: - self.opts.log.info("\t '%s' found in '%s' (%s)" % - (rule['pattern'], rule['field'], rule['name'])) + log_prefix_rule_match_info(rule, record) return rule['prefix'] - if False and self.opts.verbose: - self.opts.log.info("\t No prefix match found") return None def filterDbTags(self, tags): From 3b5fd30b0a8a06215bc022cff8e8026ea2e715fa Mon Sep 17 00:00:00 2001 From: GRiker Date: Sun, 5 Aug 2012 11:49:11 -0600 Subject: [PATCH 04/11] Changed to GenericRulesTable model --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 858 ++++++++++-------- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 53 +- src/calibre/library/catalogs/epub_mobi.py | 62 +- 3 files changed, 514 insertions(+), 459 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 54e8bf5db9..ac251ff801 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -17,8 +17,9 @@ from PyQt4 import QtGui from PyQt4.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QHBoxLayout, QIcon, QLabel, QLineEdit, - QPlainTextEdit, QRadioButton, QSize, QTableWidget, QTableWidgetItem, - QVBoxLayout, QWidget) + QPlainTextEdit, QRadioButton, QSize, QSizePolicy, + QTableWidget, QTableWidgetItem, + QToolButton, QVBoxLayout, QWidget) class PluginWidget(QWidget,Ui_Form): @@ -190,7 +191,9 @@ class PluginWidget(QWidget,Ui_Form): self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) # Initialize prefix rules - self.prefix_rules_initialize(prefix_rules) + self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, "prefix_rules_tw", + prefix_rules, self.eligible_custom_fields, + self.db) def options(self): # Save/return the current options @@ -212,7 +215,7 @@ class PluginWidget(QWidget,Ui_Form): elif c_type in ['spin_box']: opt_value = unicode(getattr(self, c_name).value()) elif c_type in ['table_widget']: - opt_value = self.prefix_rules_get_data() + opt_value = self.prefix_rules_table.get_data() gprefs.set(self.name + '_' + c_name, opt_value) @@ -346,393 +349,6 @@ class PluginWidget(QWidget,Ui_Form): self.merge_after.setEnabled(False) self.include_hr.setEnabled(False) - - def prefix_rules_add_row(self): - # Called when '+' clicked - self.prefix_rules_tw.setFocus() - row = self.prefix_rules_tw.currentRow() + 1 - self.prefix_rules_tw.insertRow(row) - self.prefix_rules_populate_table_row(row, self.prefix_rules_create_blank_row_data()) - self.prefix_rules_select_and_scroll_to_row(row) - self.prefix_rules_tw.resizeColumnsToContents() - # Just in case table was empty - self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) - - def prefix_rules_convert_row_to_data(self, row): - data = self.prefix_rules_create_blank_row_data() - data['ordinal'] = row - data['enabled'] = self.prefix_rules_tw.item(row,0).checkState() == Qt.Checked - data['name'] = unicode(self.prefix_rules_tw.cellWidget(row,1).text()).strip() - data['prefix'] = unicode(self.prefix_rules_tw.cellWidget(row,2).currentText()).strip() - data['field'] = unicode(self.prefix_rules_tw.cellWidget(row,3).currentText()).strip() - data['pattern'] = unicode(self.prefix_rules_tw.cellWidget(row,4).currentText()).strip() - return data - - def prefix_rules_create_blank_row_data(self): - data = {} - data['ordinal'] = -1 - data['enabled'] = False - data['name'] = 'New rule' - data['field'] = '' - data['pattern'] = '' - data['prefix'] = '' - return data - - def prefix_rules_delete_row(self): - self.prefix_rules_tw.setFocus() - rows = self.prefix_rules_tw.selectionModel().selectedRows() - if len(rows) == 0: - return - message = '

Are you sure you want to delete this rule?' - if len(rows) > 1: - message = '

Are you sure you want to delete the %d selected rules?'%len(rows) - if not question_dialog(self, _('Are you sure?'), message, show_copy_button=False): - return - first_sel_row = self.prefix_rules_tw.currentRow() - for selrow in reversed(rows): - self.prefix_rules_tw.removeRow(selrow.row()) - if first_sel_row < self.prefix_rules_tw.rowCount(): - self.prefix_rules_select_and_scroll_to_row(first_sel_row) - elif self.prefix_rules_tw.rowCount() > 0: - self.prefix_rules_select_and_scroll_to_row(first_sel_row - 1) - - def prefix_rules_generate_prefix_list(self): - def prefix_sorter(item): - key = item - if item[0] == "_": - key = 'zzz' + item - return key - - - # Create a list of prefixes for user selection - raw_prefix_list = [ - ('Ampersand',u'&'), - ('Angle left double',u'\u00ab'), - ('Angle left',u'\u2039'), - ('Angle right double',u'\u00bb'), - ('Angle right',u'\u203a'), - ('Arrow carriage return',u'\u21b5'), - ('Arrow double',u'\u2194'), - ('Arrow down',u'\u2193'), - ('Arrow left',u'\u2190'), - ('Arrow right',u'\u2192'), - ('Arrow up',u'\u2191'), - ('Asterisk',u'*'), - ('At sign',u'@'), - ('Bullet smallest',u'\u22c5'), - ('Bullet small',u'\u00b7'), - ('Bullet',u'\u2022'), - ('Cards clubs',u'\u2663'), - ('Cards diamonds',u'\u2666'), - ('Cards hearts',u'\u2665'), - ('Cards spades',u'\u2660'), - ('Caret',u'^'), - ('Checkmark',u'\u2713'), - ('Copyright circle c',u'\u00a9'), - ('Copyright circle r',u'\u00ae'), - ('Copyright trademark',u'\u2122'), - ('Currency cent',u'\u00a2'), - ('Currency dollar',u'$'), - ('Currency euro',u'\u20ac'), - ('Currency pound',u'\u00a3'), - ('Currency yen',u'\u00a5'), - ('Dagger double',u'\u2021'), - ('Dagger',u'\u2020'), - ('Degree',u'\u00b0'), - ('Dots3',u'\u2234'), - ('Hash',u'#'), - ('Infinity',u'\u221e'), - ('Lozenge',u'\u25ca'), - ('Math divide',u'\u00f7'), - ('Math empty',u'\u2205'), - ('Math equals',u'='), - ('Math minus',u'\u2212'), - ('Math plus circled',u'\u2295'), - ('Math times circled',u'\u2297'), - ('Math times',u'\u00d7'), - ('O slash',u'\u00d8'), - ('Paragraph',u'\u00b6'), - ('Percent',u'%'), - ('Plus-or-minus',u'\u00b1'), - ('Plus',u'+'), - ('Punctuation colon',u':'), - ('Punctuation colon-semi',u';'), - ('Punctuation exclamation',u'!'), - ('Punctuation question',u'?'), - ('Punctuation period',u'.'), - ('Punctuation slash back',u'\\'), - ('Punctuation slash forward',u'/'), - ('Section',u'\u00a7'), - ('Tilde',u'~'), - ('Vertical bar',u'|'), - ('Vertical bar broken',u'\u00a6'), - ('_0',u'0'), - ('_1',u'1'), - ('_2',u'2'), - ('_3',u'3'), - ('_4',u'4'), - ('_5',u'5'), - ('_6',u'6'), - ('_7',u'7'), - ('_8',u'8'), - ('_9',u'9'), - ('_A',u'A'), - ('_B',u'B'), - ('_C',u'C'), - ('_D',u'D'), - ('_E',u'E'), - ('_F',u'F'), - ('_G',u'G'), - ('_H',u'H'), - ('_I',u'I'), - ('_J',u'J'), - ('_K',u'K'), - ('_L',u'L'), - ('_M',u'M'), - ('_N',u'N'), - ('_O',u'O'), - ('_P',u'P'), - ('_Q',u'Q'), - ('_R',u'R'), - ('_S',u'S'), - ('_T',u'T'), - ('_U',u'U'), - ('_V',u'V'), - ('_W',u'W'), - ('_X',u'X'), - ('_Y',u'Y'), - ('_Z',u'Z'), - ('_a',u'a'), - ('_b',u'b'), - ('_c',u'c'), - ('_d',u'd'), - ('_e',u'e'), - ('_f',u'f'), - ('_g',u'g'), - ('_h',u'h'), - ('_i',u'i'), - ('_j',u'j'), - ('_k',u'k'), - ('_l',u'l'), - ('_m',u'm'), - ('_n',u'n'), - ('_o',u'o'), - ('_p',u'p'), - ('_q',u'q'), - ('_r',u'r'), - ('_s',u's'), - ('_t',u't'), - ('_u',u'u'), - ('_v',u'v'), - ('_w',u'w'), - ('_x',u'x'), - ('_y',u'y'), - ('_z',u'z'), - ] - raw_prefix_list = sorted(raw_prefix_list, key=prefix_sorter) - self.prefixes = [x[1] for x in raw_prefix_list] - - def prefix_rules_get_data(self): - data_items = [] - for row in range(self.prefix_rules_tw.rowCount()): - data = self.prefix_rules_convert_row_to_data(row) - data_items.append( - {'ordinal':data['ordinal'], - 'enabled':data['enabled'], - 'name':data['name'], - 'field':data['field'], - 'pattern':data['pattern'], - 'prefix':data['prefix']}) - return data_items - - def prefix_rules_initialize(self, prefix_rules): - # Assign icons to buttons, hook clicks - self.move_rule_up_tb.setToolTip('Move rule up') - self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) - self.move_rule_up_tb.clicked.connect(self.prefix_rules_move_row_up) - - self.add_rule_tb.setToolTip('Add a new rule') - self.add_rule_tb.setIcon(QIcon(I('plus.png'))) - self.add_rule_tb.clicked.connect(self.prefix_rules_add_row) - - self.delete_rule_tb.setToolTip('Delete selected rule') - self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) - self.delete_rule_tb.clicked.connect(self.prefix_rules_delete_row) - - self.move_rule_down_tb.setToolTip('Move rule down') - self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) - self.move_rule_down_tb.clicked.connect(self.prefix_rules_move_row_down) - - # Configure the QTableWidget - # enabled/prefix/rule name/source field/pattern - self.prefix_rules_tw.clear() - header_labels = ['','Name','Prefix','Source','Pattern'] - self.prefix_rules_tw.setColumnCount(len(header_labels)) - self.prefix_rules_tw.setHorizontalHeaderLabels(header_labels) - self.prefix_rules_tw.setSortingEnabled(False) - self.prefix_rules_tw.setSelectionBehavior(QAbstractItemView.SelectRows) - - # Generate the prefix list - self.prefix_rules_generate_prefix_list() - - # Populate the table - self.prefix_rules_populate(prefix_rules) - self.prefix_rules_tw.resizeColumnsToContents() - self.prefix_rules_resize_name(1.5) - self.prefix_rules_tw.horizontalHeader().setStretchLastSection(True) - - def prefix_rules_move_row_down(self): - self.prefix_rules_tw.setFocus() - rows = self.prefix_rules_tw.selectionModel().selectedRows() - if len(rows) == 0: - return - last_sel_row = rows[-1].row() - if last_sel_row == self.prefix_rules_tw.rowCount() - 1: - return - - self.prefix_rules_tw.blockSignals(True) - for selrow in reversed(rows): - dest_row = selrow.row() + 1 - src_row = selrow.row() - - # Save the contents of the destination row - saved_data = self.prefix_rules_convert_row_to_data(dest_row) - - # Remove the destination row - self.prefix_rules_tw.removeRow(dest_row) - - # Insert a new row at the original location - self.prefix_rules_tw.insertRow(src_row) - - # Populate it with the saved data - self.prefix_rules_populate_table_row(src_row, saved_data) - self.blockSignals(False) - scroll_to_row = last_sel_row + 1 - if scroll_to_row < self.prefix_rules_tw.rowCount() - 1: - scroll_to_row = scroll_to_row + 1 - self.prefix_rules_tw.scrollToItem(self.prefix_rules_tw.item(scroll_to_row, 0)) - - def prefix_rules_move_row_up(self): - self.prefix_rules_tw.setFocus() - rows = self.prefix_rules_tw.selectionModel().selectedRows() - if len(rows) == 0: - return - first_sel_row = rows[0].row() - if first_sel_row <= 0: - return - self.prefix_rules_tw.blockSignals(True) - for selrow in rows: - # Save the row above - saved_data = self.prefix_rules_convert_row_to_data(selrow.row() - 1) - - # Add a row below us with the source data - self.prefix_rules_tw.insertRow(selrow.row() + 1) - self.prefix_rules_populate_table_row(selrow.row() + 1, saved_data) - - # Delete the row above - self.prefix_rules_tw.removeRow(selrow.row() - 1) - self.blockSignals(False) - - scroll_to_row = first_sel_row - 1 - if scroll_to_row > 0: - scroll_to_row = scroll_to_row - 1 - self.prefix_rules_tw.scrollToItem(self.prefix_rules_tw.item(scroll_to_row, 0)) - - def prefix_rules_populate(self,rules): - # Format of rules list is different if default values vs retrieved JSON - # Hack to normalize list style - if type(rules[0]) is list: - rules = rules[0] - self.prefix_rules_tw.setFocus() - rules = sorted(rules, key=lambda k: k['ordinal']) - for row, rule in enumerate(rules): - self.prefix_rules_tw.insertRow(row) - self.prefix_rules_select_and_scroll_to_row(row) - self.prefix_rules_populate_table_row(row, rule) - self.prefix_rules_tw.selectRow(0) - - def prefix_rules_populate_table_row(self, row, data): - - def set_prefix_field_in_row(row, col, field=''): - prefix_combo = PrefixRulesComboBox(self, self.prefixes, field) - self.prefix_rules_tw.setCellWidget(row, col, prefix_combo) - - def set_rule_name_in_row(row, col, name=''): - rule_name = QLineEdit(name) - rule_name.home(False) - rule_name.editingFinished.connect(self.prefix_rules_rule_name_edited) - self.prefix_rules_tw.setCellWidget(row, col, rule_name) - - def set_source_field_in_row(row, col, field=''): - source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) - source_combo.currentIndexChanged.connect(partial(self.prefix_rules_source_index_changed, source_combo, row)) - #source_combo.currentIndexChanged.connect(self.prefix_rules_source_index_changed, source_combo, row) - self.prefix_rules_tw.setCellWidget(row, col, source_combo) - return source_combo - - - # Entry point - self.prefix_rules_tw.blockSignals(True) - #print("prefix_rules_populate_table_row processing rule:\n%s\n" % data) - - # Column 0: Enabled - self.prefix_rules_tw.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) - - # Column 1: Rule name - #rule_name = QTableWidgetItem(data['name']) - #self.prefix_rules_tw.setItem(row, 1, rule_name) - set_rule_name_in_row(row, 1, name=data['name']) - - # Column 2: Prefix - set_prefix_field_in_row(row, 2, field=data['prefix']) - - # Column 3: Source field - source_combo = set_source_field_in_row(row, 3, field=data['field']) - - # Column 4: Pattern - # The contents of the Pattern field is driven by the Source field - self.prefix_rules_source_index_changed(source_combo, row, 4, pattern=data['pattern']) - - self.prefix_rules_tw.blockSignals(False) - - def prefix_rules_resize_name(self, scale): - current_width = self.prefix_rules_tw.columnWidth(1) - self.prefix_rules_tw.setColumnWidth(1, min(225,int(current_width * scale))) - - def prefix_rules_rule_name_edited(self): - current_row = self.prefix_rules_tw.currentRow() - self.prefix_rules_tw.cellWidget(current_row,1).home(False) - self.prefix_rules_tw.setFocus() - self.prefix_rules_select_and_scroll_to_row(current_row) - - def prefix_rules_select_and_scroll_to_row(self, row): - self.prefix_rules_tw.selectRow(row) - self.prefix_rules_tw.scrollToItem(self.prefix_rules_tw.currentItem()) - - def prefix_rules_source_index_changed(self, combo, row, col, pattern=''): - # Populate the Pattern field based upon the Source field - - source_field = str(combo.currentText()) - if source_field == '': - values = [] - elif source_field == 'Tags': - values = sorted(self.db.all_tags(), key=sort_key) - else: - if self.eligible_custom_fields[source_field]['datatype'] in ['enumeration', 'text']: - values = self.db.all_custom(self.db.field_metadata.key_to_label( - self.eligible_custom_fields[source_field]['field'])) - values = sorted(values, key=sort_key) - elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']: - values = ['True','False','unspecified'] - elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: - values = ['any date','unspecified'] - elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: - values = ['any value','unspecified'] - - values_combo = PrefixRulesComboBox(self, values, pattern) - self.prefix_rules_tw.setCellWidget(row, 4, values_combo) - - def read_source_field_changed(self,new_index): ''' Process changes in the read_source_field combo box @@ -887,3 +503,463 @@ class PrefixRulesComboBox(NoWheelComboBox): self.setCurrentIndex(idx) else: self.setCurrentIndex(0) + +class GenericRulesTable(QTableWidget): + ''' + Generic methods for managing rows + Add QTableWidget, controls to parent QGroupBox + placeholders for basic methods to be overriden + ''' + + def __init__(self, parent_gb_hl, object_name, prefix_rules, eligible_custom_fields, db): + self.prefix_rules = prefix_rules + self.eligible_custom_fields = eligible_custom_fields + self.db = db + QTableWidget.__init__(self) + self.setObjectName(object_name) + self.layout = parent_gb_hl + + # Add ourselves to the layout + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(sizePolicy) + self.setMaximumSize(QSize(16777215, 118)) + self.setColumnCount(0) + self.setRowCount(0) + self.layout.addWidget(self) + + + self._init_table_widget() + self._init_controls() + self._initialize() + + def _init_controls(self): + # Add the control set + vbl = QVBoxLayout() + self.move_rule_up_tb = QToolButton() + self.move_rule_up_tb.setObjectName("move_rule_up_tb") + self.move_rule_up_tb.setToolTip('Move rule up') + self.move_rule_up_tb.setIcon(QIcon(I('arrow-up.png'))) + self.move_rule_up_tb.clicked.connect(self.move_row_up) + vbl.addWidget(self.move_rule_up_tb) + + self.add_rule_tb = QToolButton() + self.add_rule_tb.setObjectName("add_rule_tb") + self.add_rule_tb.setToolTip('Add a new rule') + self.add_rule_tb.setIcon(QIcon(I('plus.png'))) + self.add_rule_tb.clicked.connect(self.add_row) + vbl.addWidget(self.add_rule_tb) + + self.delete_rule_tb = QToolButton() + self.delete_rule_tb.setObjectName("delete_rule_tb") + self.delete_rule_tb.setToolTip('Delete selected rule') + self.delete_rule_tb.setIcon(QIcon(I('list_remove.png'))) + self.delete_rule_tb.clicked.connect(self.delete_row) + vbl.addWidget(self.delete_rule_tb) + + self.move_rule_down_tb = QToolButton() + self.move_rule_down_tb.setObjectName("move_rule_down_tb") + self.move_rule_down_tb.setToolTip('Move rule down') + self.move_rule_down_tb.setIcon(QIcon(I('arrow-down.png'))) + self.move_rule_down_tb.clicked.connect(self.move_row_down) + vbl.addWidget(self.move_rule_down_tb) + + self.layout.addLayout(vbl) + + def _init_table_widget(self): + ''' + Override this in the specific instance + ''' + pass + + def _initialize(self): + ''' + Override this in the specific instance + ''' + pass + + def add_row(self): + self.setFocus() + row = self.currentRow() + 1 + self.insertRow(row) + self.populate_table_row(row, self.create_blank_row_data()) + self.select_and_scroll_to_row(row) + self.resizeColumnsToContents() + # Just in case table was empty + self.horizontalHeader().setStretchLastSection(True) + + def convert_row_to_data(self): + ''' + override + ''' + pass + + def create_blank_row_data(self): + ''' + ovverride + ''' + pass + + def delete_row(self): + self.setFocus() + rows = self.selectionModel().selectedRows() + if len(rows) == 0: + return + message = '

Are you sure you want to delete this rule?' + if len(rows) > 1: + message = '

Are you sure you want to delete the %d selected rules?'%len(rows) + if not question_dialog(self, _('Are you sure?'), message, show_copy_button=False): + return + first_sel_row = self.currentRow() + for selrow in reversed(rows): + self.removeRow(selrow.row()) + if first_sel_row < self.rowCount(): + self.select_and_scroll_to_row(first_sel_row) + elif self.rowCount() > 0: + self.select_and_scroll_to_row(first_sel_row - 1) + + def get_data(self): + pass + + def move_row_down(self): + self.setFocus() + rows = self.selectionModel().selectedRows() + if len(rows) == 0: + return + last_sel_row = rows[-1].row() + if last_sel_row == self.rowCount() - 1: + return + + self.blockSignals(True) + for selrow in reversed(rows): + dest_row = selrow.row() + 1 + src_row = selrow.row() + + # Save the contents of the destination row + saved_data = self.convert_row_to_data(dest_row) + + # Remove the destination row + self.removeRow(dest_row) + + # Insert a new row at the original location + self.insertRow(src_row) + + # Populate it with the saved data + self.populate_table_row(src_row, saved_data) + self.blockSignals(False) + scroll_to_row = last_sel_row + 1 + if scroll_to_row < self.rowCount() - 1: + scroll_to_row = scroll_to_row + 1 + self.scrollToItem(self.item(scroll_to_row, 0)) + + def move_row_up(self): + self.setFocus() + rows = self.selectionModel().selectedRows() + if len(rows) == 0: + return + first_sel_row = rows[0].row() + if first_sel_row <= 0: + return + self.blockSignals(True) + for selrow in rows: + # Save the row above + saved_data = self.convert_row_to_data(selrow.row() - 1) + + # Add a row below us with the source data + self.insertRow(selrow.row() + 1) + self.populate_table_row(selrow.row() + 1, saved_data) + + # Delete the row above + self.removeRow(selrow.row() - 1) + self.blockSignals(False) + + scroll_to_row = first_sel_row - 1 + if scroll_to_row > 0: + scroll_to_row = scroll_to_row - 1 + self.scrollToItem(self.item(scroll_to_row, 0)) + + def populate_table_row(self): + ''' + override + ''' + pass + +class PrefixRules(GenericRulesTable): + + def _init_table_widget(self): + header_labels = ['','Name','Prefix','Source','Pattern'] + self.setColumnCount(len(header_labels)) + self.setHorizontalHeaderLabels(header_labels) + self.setSortingEnabled(False) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + + def _initialize(self): + self.generate_prefix_list() + self.populate() + self.resizeColumnsToContents() + self.resize_name(1.5) + self.horizontalHeader().setStretchLastSection(True) + + def convert_row_to_data(self, row): + data = self.create_blank_row_data() + data['ordinal'] = row + data['enabled'] = self.item(row,0).checkState() == Qt.Checked + data['name'] = unicode(self.cellWidget(row,1).text()).strip() + data['prefix'] = unicode(self.cellWidget(row,2).currentText()).strip() + data['field'] = unicode(self.cellWidget(row,3).currentText()).strip() + data['pattern'] = unicode(self.cellWidget(row,4).currentText()).strip() + return data + + def create_blank_row_data(self): + data = {} + data['ordinal'] = -1 + data['enabled'] = False + data['name'] = 'New rule' + data['field'] = '' + data['pattern'] = '' + data['prefix'] = '' + return data + + def generate_prefix_list(self): + def prefix_sorter(item): + key = item + if item[0] == "_": + key = 'zzz' + item + return key + + # Create a list of prefixes for user selection + raw_prefix_list = [ + ('Ampersand',u'&'), + ('Angle left double',u'\u00ab'), + ('Angle left',u'\u2039'), + ('Angle right double',u'\u00bb'), + ('Angle right',u'\u203a'), + ('Arrow carriage return',u'\u21b5'), + ('Arrow double',u'\u2194'), + ('Arrow down',u'\u2193'), + ('Arrow left',u'\u2190'), + ('Arrow right',u'\u2192'), + ('Arrow up',u'\u2191'), + ('Asterisk',u'*'), + ('At sign',u'@'), + ('Bullet smallest',u'\u22c5'), + ('Bullet small',u'\u00b7'), + ('Bullet',u'\u2022'), + ('Cards clubs',u'\u2663'), + ('Cards diamonds',u'\u2666'), + ('Cards hearts',u'\u2665'), + ('Cards spades',u'\u2660'), + ('Caret',u'^'), + ('Checkmark',u'\u2713'), + ('Copyright circle c',u'\u00a9'), + ('Copyright circle r',u'\u00ae'), + ('Copyright trademark',u'\u2122'), + ('Currency cent',u'\u00a2'), + ('Currency dollar',u'$'), + ('Currency euro',u'\u20ac'), + ('Currency pound',u'\u00a3'), + ('Currency yen',u'\u00a5'), + ('Dagger double',u'\u2021'), + ('Dagger',u'\u2020'), + ('Degree',u'\u00b0'), + ('Dots3',u'\u2234'), + ('Hash',u'#'), + ('Infinity',u'\u221e'), + ('Lozenge',u'\u25ca'), + ('Math divide',u'\u00f7'), + ('Math empty',u'\u2205'), + ('Math equals',u'='), + ('Math minus',u'\u2212'), + ('Math plus circled',u'\u2295'), + ('Math times circled',u'\u2297'), + ('Math times',u'\u00d7'), + ('O slash',u'\u00d8'), + ('Paragraph',u'\u00b6'), + ('Percent',u'%'), + ('Plus-or-minus',u'\u00b1'), + ('Plus',u'+'), + ('Punctuation colon',u':'), + ('Punctuation colon-semi',u';'), + ('Punctuation exclamation',u'!'), + ('Punctuation question',u'?'), + ('Punctuation period',u'.'), + ('Punctuation slash back',u'\\'), + ('Punctuation slash forward',u'/'), + ('Section',u'\u00a7'), + ('Tilde',u'~'), + ('Vertical bar',u'|'), + ('Vertical bar broken',u'\u00a6'), + ('_0',u'0'), + ('_1',u'1'), + ('_2',u'2'), + ('_3',u'3'), + ('_4',u'4'), + ('_5',u'5'), + ('_6',u'6'), + ('_7',u'7'), + ('_8',u'8'), + ('_9',u'9'), + ('_A',u'A'), + ('_B',u'B'), + ('_C',u'C'), + ('_D',u'D'), + ('_E',u'E'), + ('_F',u'F'), + ('_G',u'G'), + ('_H',u'H'), + ('_I',u'I'), + ('_J',u'J'), + ('_K',u'K'), + ('_L',u'L'), + ('_M',u'M'), + ('_N',u'N'), + ('_O',u'O'), + ('_P',u'P'), + ('_Q',u'Q'), + ('_R',u'R'), + ('_S',u'S'), + ('_T',u'T'), + ('_U',u'U'), + ('_V',u'V'), + ('_W',u'W'), + ('_X',u'X'), + ('_Y',u'Y'), + ('_Z',u'Z'), + ('_a',u'a'), + ('_b',u'b'), + ('_c',u'c'), + ('_d',u'd'), + ('_e',u'e'), + ('_f',u'f'), + ('_g',u'g'), + ('_h',u'h'), + ('_i',u'i'), + ('_j',u'j'), + ('_k',u'k'), + ('_l',u'l'), + ('_m',u'm'), + ('_n',u'n'), + ('_o',u'o'), + ('_p',u'p'), + ('_q',u'q'), + ('_r',u'r'), + ('_s',u's'), + ('_t',u't'), + ('_u',u'u'), + ('_v',u'v'), + ('_w',u'w'), + ('_x',u'x'), + ('_y',u'y'), + ('_z',u'z'), + ] + raw_prefix_list = sorted(raw_prefix_list, key=prefix_sorter) + self.prefix_list = [x[1] for x in raw_prefix_list] + + def get_data(self): + data_items = [] + for row in range(self.rowCount()): + data = self.convert_row_to_data(row) + data_items.append( + {'ordinal':data['ordinal'], + 'enabled':data['enabled'], + 'name':data['name'], + 'field':data['field'], + 'pattern':data['pattern'], + 'prefix':data['prefix']}) + return data_items + + def populate(self): + # Format of rules list is different if default values vs retrieved JSON + # Hack to normalize list style + rules = self.prefix_rules + if type(rules[0]) is list: + rules = rules[0] + self.setFocus() + rules = sorted(rules, key=lambda k: k['ordinal']) + for row, rule in enumerate(rules): + self.insertRow(row) + self.select_and_scroll_to_row(row) + self.populate_table_row(row, rule) + self.selectRow(0) + + def populate_table_row(self, row, data): + + def set_prefix_field_in_row(row, col, field=''): + prefix_combo = PrefixRulesComboBox(self, self.prefix_list, field) + self.setCellWidget(row, col, prefix_combo) + + def set_rule_name_in_row(row, col, name=''): + rule_name = QLineEdit(name) + rule_name.home(False) + rule_name.editingFinished.connect(self.rule_name_edited) + self.setCellWidget(row, col, rule_name) + + def set_source_field_in_row(row, col, field=''): + source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) + source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row)) + self.setCellWidget(row, col, source_combo) + return source_combo + + + # Entry point + self.blockSignals(True) + #print("prefix_rules_populate_table_row processing rule:\n%s\n" % data) + + # Column 0: Enabled + self.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) + + # Column 1: Rule name + #rule_name = QTableWidgetItem(data['name']) + set_rule_name_in_row(row, 1, name=data['name']) + + # Column 2: Prefix + set_prefix_field_in_row(row, 2, field=data['prefix']) + + # Column 3: Source field + source_combo = set_source_field_in_row(row, 3, field=data['field']) + + # Column 4: Pattern + # The contents of the Pattern field is driven by the Source field + self.source_index_changed(source_combo, row, 4, pattern=data['pattern']) + + self.blockSignals(False) + + def resize_name(self, scale): + current_width = self.columnWidth(1) + self.setColumnWidth(1, min(225,int(current_width * scale))) + + def rule_name_edited(self): + current_row = self.currentRow() + self.cellWidget(current_row,1).home(False) + self.setFocus() + self.select_and_scroll_to_row(current_row) + + def select_and_scroll_to_row(self, row): + self.selectRow(row) + self.scrollToItem(self.currentItem()) + + def source_index_changed(self, combo, row, col, pattern=''): + # Populate the Pattern field based upon the Source field + + source_field = str(combo.currentText()) + if source_field == '': + values = [] + elif source_field == 'Tags': + values = sorted(self.db.all_tags(), key=sort_key) + else: + if self.eligible_custom_fields[source_field]['datatype'] in ['enumeration', 'text']: + values = self.db.all_custom(self.db.field_metadata.key_to_label( + self.eligible_custom_fields[source_field]['field'])) + values = sorted(values, key=sort_key) + elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']: + values = ['True','False','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: + values = ['any date','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: + values = ['any value','unspecified'] + + values_combo = PrefixRulesComboBox(self, values, pattern) + self.setCellWidget(row, 4, values_combo) + diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index eb39415a81..bf10cacd38 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -327,7 +327,7 @@ Default: ~,Catalog - + 0 @@ -342,56 +342,7 @@ Default: ~,Catalog - - - - - - 0 - 0 - - - - - 16777215 - 118 - - - - - - - - - - ... - - - - - - - ... - - - - - - - ... - - - - - - - ... - - - - - - + diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index ee43d21357..a7da345dc2 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -47,28 +47,44 @@ class EPUB_MOBI(CatalogPlugin): "of the conversion process a bug is occurring.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), - Option('--exclude-book-marker', - default=':', - dest='exclude_book_marker', - action = None, - help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n" - "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n" - "Default: '%default'\n" - "Applies to ePub, MOBI output formats")), Option('--exclude-genre', default='\[.+\]', dest='exclude_genre', action = None, help=_("Regex describing tags to exclude as genres.\n" "Default: '%default' excludes bracketed tags, e.g. '[]'\n" "Applies to: ePub, MOBI output formats")), - Option('--exclude-tags', - default=('~,'+_('Catalog')), - dest='exclude_tags', - action = None, - help=_("Comma-separated list of tag words indicating book should be excluded from output. " - "For example: 'skip' will match 'skip this book' and 'Skip will like this'. " - "Default: '%default'\n" - "Applies to: ePub, MOBI output formats")), + +# Option('--exclude-book-marker', +# default=':', +# dest='exclude_book_marker', +# action = None, +# help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n" +# "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n" +# "Default: '%default'\n" +# "Applies to ePub, MOBI output formats")), +# Option('--exclude-tags', +# default=('~,Catalog'), +# dest='exclude_tags', +# action = None, +# help=_("Comma-separated list of tag words indicating book should be excluded from output. " +# "For example: 'skip' will match 'skip this book' and 'Skip will like this'. " +# "Default:'%default'\n" +# "Applies to: ePub, MOBI output formats")), + + Option('--exclusion-rules', + default="(('Excluded tags','Tags','~,Catalog'),)", + dest='exclusion_rules', + action=None, + help=_("Specifies the rules used to exclude books from the generated catalog.\n" + "The model for an exclusion rule is either\n('','Tags','') or\n" + "('','','').\n" + "For example:\n" + "(('Archived books','#status','Archived'),)\n" + "will exclude a book with a value of 'Archived' in the custom column 'status'.\n" + "When multiple rules are defined, all rules will be applied.\n" + "Default: \n" + '"' + '%default' + '"' + "\n" + "Applies to ePub, MOBI output formats")), + Option('--generate-authors', default=False, dest='generate_authors', @@ -142,7 +158,7 @@ class EPUB_MOBI(CatalogPlugin): help=_("Specifies the rules used to include prefixes indicating read books, wishlist items and other user-specifed prefixes.\n" "The model for a prefix rule is ('','','','').\n" "When multiple rules are defined, the first matching rule will be used.\n" - "Default: '%default'\n" + "Default:\n" + '"' + '%default' + '"' + "\n" "Applies to ePub, MOBI output formats")), Option('--thumb-width', default='1.0', @@ -285,6 +301,17 @@ class EPUB_MOBI(CatalogPlugin): if len(rule) != 4: log.error("incorrect number of args for --prefix-rules: %s" % repr(rule)) + # eval exclusion_rules if passed from command line + if type(opts.exclusion_rules) is not tuple: + try: + opts.exclusion_rules = eval(opts.exclusion_rules) + except: + log.error("malformed --exclusion-rules: %s" % opts.exclusion_rules) + raise + for rule in opts.exclusion_rules: + if len(rule) != 3: + log.error("incorrect number of args for --exclusion-rules: %s" % repr(rule)) + # Display opts keys = opts_dict.keys() keys.sort() @@ -292,6 +319,7 @@ class EPUB_MOBI(CatalogPlugin): for key in keys: if key in ['catalog_title','authorClip','connected_kindle','descriptionClip', 'exclude_book_marker','exclude_genre','exclude_tags', + 'exclusion_rules', 'header_note_source_field','merge_comments', 'output_profile','prefix_rules','read_book_marker', 'search_text','sort_by','sort_descriptions_by_author','sync', From 24aab4fb5795e557ab1468a693907e310edfdd25 Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 6 Aug 2012 06:17:18 -0600 Subject: [PATCH 05/11] exclusion_rules_table added --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 249 +++++++++++++++--- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 190 +++---------- src/calibre/library/catalogs/epub_mobi.py | 22 +- .../library/catalogs/epub_mobi_builder.py | 93 +++++-- 4 files changed, 315 insertions(+), 239 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index ac251ff801..8a2035c76f 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -38,7 +38,7 @@ class PluginWidget(QWidget,Ui_Form): self._initControlArrays() def _initControlArrays(self): - + # Default values for controls CheckBoxControls = [] ComboBoxControls = [] DoubleSpinBoxControls = [] @@ -72,13 +72,27 @@ class PluginWidget(QWidget,Ui_Form): # LineEditControls option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit']) - option_fields += zip(['exclude_pattern'],[None],['line_edit']) - option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit']) + #***option_fields += zip(['exclude_pattern'],[None],['line_edit']) + #***option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit']) # SpinBoxControls option_fields += zip(['thumb_width'],[1.00],['spin_box']) - # Prefix rules TableWidget + # Exclusion rules + option_fields += zip(['exclusion_rules_tw','exclusion_rules_tw'], + [{'ordinal':0, + 'enabled':True, + 'name':'Catalogs', + 'field':'Tags', + 'pattern':'Catalog'}, + {'ordinal':1, + 'enabled':False, + 'name':'New rule', + 'field':'', + 'pattern':''}], + ['table_widget','table_widget']) + + # Prefix rules option_fields += zip(['prefix_rules_tw','prefix_rules_tw','prefix_rules_tw'], [{'ordinal':0, 'enabled':True, @@ -123,13 +137,13 @@ class PluginWidget(QWidget,Ui_Form): ['exclude_source_field','header_note_source_field', 'merge_source_field'] LineEditControls (c_type: line_edit): - ['exclude_genre','exclude_pattern','exclude_tags'] + ['exclude_genre'] RadioButtonControls (c_type: radio_button): ['merge_before','merge_after'] SpinBoxControls (c_type: spin_box): ['thumb_width'] TableWidgetControls (c_type: table_widget): - ['prefix_rules_tw'] + ['exclusion_rules_tw','prefix_rules_tw'] ''' self.name = name @@ -139,6 +153,7 @@ class PluginWidget(QWidget,Ui_Form): # Update dialog fields from stored options + exclusion_rules = [] prefix_rules = [] for opt in self.OPTION_FIELDS: c_name, c_def, c_type = opt @@ -159,16 +174,22 @@ class PluginWidget(QWidget,Ui_Form): getattr(self, c_name).setChecked(opt_value) elif c_type in ['spin_box']: getattr(self, c_name).setValue(float(opt_value)) + elif c_type in ['table_widget'] and c_name == 'exclusion_rules_tw': + if opt_value not in exclusion_rules: + exclusion_rules.append(opt_value) elif c_type in ['table_widget'] and c_name == 'prefix_rules_tw': if opt_value not in prefix_rules: prefix_rules.append(opt_value) + ''' + *** # Init self.exclude_source_field_name self.exclude_source_field_name = '' cs = unicode(self.exclude_source_field.currentText()) if cs > '': exclude_source_spec = self.exclude_source_fields[cs] self.exclude_source_field_name = exclude_source_spec['field'] + ''' # Init self.merge_source_field_name self.merge_source_field_name = '' @@ -190,10 +211,13 @@ class PluginWidget(QWidget,Ui_Form): # Hook changes to Description section self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) + # Initialize exclusion rules + self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb_hl, + "exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db) + # Initialize prefix rules - self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, "prefix_rules_tw", - prefix_rules, self.eligible_custom_fields, - self.db) + self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, + "prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db) def options(self): # Save/return the current options @@ -204,6 +228,8 @@ class PluginWidget(QWidget,Ui_Form): opts_dict = {} # Save values to gprefs prefix_rules_processed = False + exclusion_rules_processed = False + for opt in self.OPTION_FIELDS: c_name, c_def, c_type = opt if c_type in ['check_box', 'radio_button']: @@ -215,7 +241,10 @@ class PluginWidget(QWidget,Ui_Form): elif c_type in ['spin_box']: opt_value = unicode(getattr(self, c_name).value()) elif c_type in ['table_widget']: - opt_value = self.prefix_rules_table.get_data() + if c_name == 'prefix_rules_tw': + opt_value = self.prefix_rules_table.get_data() + if c_name == 'exclusion_rules_tw': + opt_value = self.exclusion_rules_table.get_data() gprefs.set(self.name + '_' + c_name, opt_value) @@ -246,19 +275,49 @@ class PluginWidget(QWidget,Ui_Form): pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix']) rule_set.append(pr) - opt_value = tuple(rule_set) opts_dict['prefix_rules'] = opt_value prefix_rules_processed = True + elif c_name == 'exclusion_rules_tw': + if exclusion_rules_processed: + continue + rule_set = [] + for rule in opt_value: + # Test for empty name/field/pattern/prefix, continue + # If pattern = any or unspecified, convert to regex + if not rule['enabled']: + continue + elif not rule['field'] or not rule['pattern']: + continue + else: + if rule['field'] != 'Tags': + # Look up custom column name + #print(self.eligible_custom_fields[rule['field']]['field']) + rule['field'] = self.eligible_custom_fields[rule['field']]['field'] + if rule['pattern'].startswith('any'): + rule['pattern'] = '.*' + elif rule['pattern'] == 'unspecified': + rule['pattern'] = 'None' + + pr = (rule['name'],rule['field'],rule['pattern']) + rule_set.append(pr) + + opt_value = tuple(rule_set) + opts_dict['exclusion_rules'] = opt_value + exclusion_rules_processed = True + else: opts_dict[c_name] = opt_value + ''' + *** # Generate markers for hybrids #opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field_name, # self.read_pattern.text()) opts_dict['exclude_book_marker'] = "%s:%s" % (self.exclude_source_field_name, self.exclude_pattern.text()) + ''' # Generate specs for merge_comments, header_note_source_field checked = '' @@ -306,6 +365,8 @@ class PluginWidget(QWidget,Ui_Form): if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: custom_fields[field_md['name']] = {'field':custom_field, 'datatype':field_md['datatype']} + ''' + *** # Blank field first self.exclude_source_field.addItem('') # Add the sorted eligible fields to the combo box @@ -313,7 +374,7 @@ class PluginWidget(QWidget,Ui_Form): self.exclude_source_field.addItem(cf) self.exclude_source_fields = custom_fields self.exclude_source_field.currentIndexChanged.connect(self.exclude_source_field_changed) - + ''' # Populate the 'Header note' combo box custom_fields = {} @@ -488,7 +549,7 @@ class NoWheelComboBox(QComboBox): # Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid event.ignore() -class PrefixRulesComboBox(NoWheelComboBox): +class ComboBox(NoWheelComboBox): # Caller is responsible for providing the list in the preferred order def __init__(self, parent, items, selected_text,insert_blank=True): NoWheelComboBox.__init__(self, parent) @@ -511,8 +572,8 @@ class GenericRulesTable(QTableWidget): placeholders for basic methods to be overriden ''' - def __init__(self, parent_gb_hl, object_name, prefix_rules, eligible_custom_fields, db): - self.prefix_rules = prefix_rules + def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): + self.rules = rules self.eligible_custom_fields = eligible_custom_fields self.db = db QTableWidget.__init__(self) @@ -525,12 +586,11 @@ class GenericRulesTable(QTableWidget): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) - self.setMaximumSize(QSize(16777215, 118)) + self.setMaximumSize(QSize(16777215, 114)) self.setColumnCount(0) self.setRowCount(0) self.layout.addWidget(self) - self._init_table_widget() self._init_controls() self._initialize() @@ -686,10 +746,138 @@ class GenericRulesTable(QTableWidget): ''' pass + def resize_name(self, scale): + current_width = self.columnWidth(1) + self.setColumnWidth(1, min(225,int(current_width * scale))) + + def rule_name_edited(self): + current_row = self.currentRow() + self.cellWidget(current_row,1).home(False) + self.setFocus() + self.select_and_scroll_to_row(current_row) + + def select_and_scroll_to_row(self, row): + self.selectRow(row) + self.scrollToItem(self.currentItem()) + +class ExclusionRules(GenericRulesTable): + + def _init_table_widget(self): + header_labels = ['','Name','Field','Value'] + self.setColumnCount(len(header_labels)) + self.setHorizontalHeaderLabels(header_labels) + self.setSortingEnabled(False) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + + def _initialize(self): + # Override max size (118) set in GenericRulesTable + self.setMaximumSize(QSize(16777215, 83)) + + self.populate() + self.resizeColumnsToContents() + self.resize_name(1.5) + self.horizontalHeader().setStretchLastSection(True) + + def convert_row_to_data(self, row): + data = self.create_blank_row_data() + data['ordinal'] = row + data['enabled'] = self.item(row,0).checkState() == Qt.Checked + data['name'] = unicode(self.cellWidget(row,1).text()).strip() + data['field'] = unicode(self.cellWidget(row,2).currentText()).strip() + data['pattern'] = unicode(self.cellWidget(row,3).currentText()).strip() + return data + + def create_blank_row_data(self): + data = {} + data['ordinal'] = -1 + data['enabled'] = False + data['name'] = 'New rule' + data['field'] = '' + data['pattern'] = '' + return data + + def get_data(self): + data_items = [] + for row in range(self.rowCount()): + data = self.convert_row_to_data(row) + data_items.append( + {'ordinal':data['ordinal'], + 'enabled':data['enabled'], + 'name':data['name'], + 'field':data['field'], + 'pattern':data['pattern']}) + return data_items + + def populate(self): + # Format of rules list is different if default values vs retrieved JSON + # Hack to normalize list style + rules = self.rules + if rules and type(rules[0]) is list: + rules = rules[0] + self.setFocus() + rules = sorted(rules, key=lambda k: k['ordinal']) + for row, rule in enumerate(rules): + self.insertRow(row) + self.select_and_scroll_to_row(row) + self.populate_table_row(row, rule) + self.selectRow(0) + + def populate_table_row(self, row, data): + + def set_rule_name_in_row(row, col, name=''): + rule_name = QLineEdit(name) + rule_name.home(False) + rule_name.editingFinished.connect(self.rule_name_edited) + self.setCellWidget(row, col, rule_name) + + def set_source_field_in_row(row, col, field=''): + source_combo = ComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) + source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row)) + self.setCellWidget(row, col, source_combo) + return source_combo + + # Entry point + self.blockSignals(True) + + # Column 0: Enabled + self.setItem(row, 0, CheckableTableWidgetItem(data['enabled'])) + + # Column 1: Rule name + set_rule_name_in_row(row, 1, name=data['name']) + + # Column 2: Source field + source_combo = set_source_field_in_row(row, 2, field=data['field']) + + # Column 3: Pattern + # The contents of the Pattern field is driven by the Source field + self.source_index_changed(source_combo, row, 3, pattern=data['pattern']) + + self.blockSignals(False) + + def source_index_changed(self, combo, row, col, pattern=''): + # Populate the Pattern field based upon the Source field + source_field = str(combo.currentText()) + if source_field == '': + values = [] + elif source_field == 'Tags': + values = sorted(self.db.all_tags(), key=sort_key) + else: + if self.eligible_custom_fields[source_field]['datatype'] in ['enumeration', 'text']: + values = self.db.all_custom(self.db.field_metadata.key_to_label( + self.eligible_custom_fields[source_field]['field'])) + values = sorted(values, key=sort_key) + elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']: + values = ['True','False','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: + values = ['any value','unspecified'] + + values_combo = ComboBox(self, values, pattern) + self.setCellWidget(row, 3, values_combo) + class PrefixRules(GenericRulesTable): def _init_table_widget(self): - header_labels = ['','Name','Prefix','Source','Pattern'] + header_labels = ['','Name','Prefix','Field','Value'] self.setColumnCount(len(header_labels)) self.setHorizontalHeaderLabels(header_labels) self.setSortingEnabled(False) @@ -873,8 +1061,8 @@ class PrefixRules(GenericRulesTable): def populate(self): # Format of rules list is different if default values vs retrieved JSON # Hack to normalize list style - rules = self.prefix_rules - if type(rules[0]) is list: + rules = self.rules + if rules and type(rules[0]) is list: rules = rules[0] self.setFocus() rules = sorted(rules, key=lambda k: k['ordinal']) @@ -887,7 +1075,7 @@ class PrefixRules(GenericRulesTable): def populate_table_row(self, row, data): def set_prefix_field_in_row(row, col, field=''): - prefix_combo = PrefixRulesComboBox(self, self.prefix_list, field) + prefix_combo = ComboBox(self, self.prefix_list, field) self.setCellWidget(row, col, prefix_combo) def set_rule_name_in_row(row, col, name=''): @@ -897,7 +1085,7 @@ class PrefixRules(GenericRulesTable): self.setCellWidget(row, col, rule_name) def set_source_field_in_row(row, col, field=''): - source_combo = PrefixRulesComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) + source_combo = ComboBox(self, sorted(self.eligible_custom_fields.keys(), key=sort_key), field) source_combo.currentIndexChanged.connect(partial(self.source_index_changed, source_combo, row)) self.setCellWidget(row, col, source_combo) return source_combo @@ -926,22 +1114,9 @@ class PrefixRules(GenericRulesTable): self.blockSignals(False) - def resize_name(self, scale): - current_width = self.columnWidth(1) - self.setColumnWidth(1, min(225,int(current_width * scale))) - - def rule_name_edited(self): - current_row = self.currentRow() - self.cellWidget(current_row,1).home(False) - self.setFocus() - self.select_and_scroll_to_row(current_row) - - def select_and_scroll_to_row(self, row): - self.selectRow(row) - self.scrollToItem(self.currentItem()) - def source_index_changed(self, combo, row, col, pattern=''): # Populate the Pattern field based upon the Source field + # row, col are the control that changed source_field = str(combo.currentText()) if source_field == '': @@ -960,6 +1135,6 @@ class PrefixRules(GenericRulesTable): elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: values = ['any value','unspecified'] - values_combo = PrefixRulesComboBox(self, values, pattern) + values_combo = ComboBox(self, values, pattern) self.setCellWidget(row, 4, values_combo) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index bf10cacd38..29ca39bf10 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -184,7 +184,7 @@ p, li { white-space: pre-wrap; } - + 0 @@ -198,130 +198,14 @@ p, li { white-space: pre-wrap; } - Books matching either pattern will not be included in generated catalog. + Matching books will not be included in generated catalog. Excluded books - - - QFormLayout::FieldsStayAtSizeHint - - - - - - - - 0 - 0 - - - - - 175 - 0 - - - - - 200 - 16777215 - - - - Tags to &exclude - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - exclude_tags - - - - - - - - 0 - 0 - - - - <p>Comma-separated list of tags to exclude. -Default: ~,Catalog - - - - - - - - - - - - 175 - 0 - - - - - 200 - 16777215 - - - - &Column/value - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - true - - - exclude_source_field - - - - - - - - 0 - 0 - - - - Column containing additional exclusion criteria - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - 18 - - - - - - - - 150 - 0 - - - - Exclusion pattern - - - - + + + @@ -335,7 +219,7 @@ Default: ~,Catalog - The first matching rule will be used to add a prefix to book listings in the generated catalog. + The earliest enabled matching rule will be used to add a prefix to book listings in the generated catalog. Prefix rules @@ -369,9 +253,9 @@ Default: ~,Catalog QFormLayout::FieldsStayAtSizeHint - + - + 175 @@ -385,16 +269,13 @@ Default: ~,Catalog - &Thumbnail width + &Thumb width Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - true - - thumb_width + merge_source_field @@ -406,6 +287,12 @@ Default: ~,Catalog 0 + + + 137 + 16777215 + + Size hint for Description cover thumbnails @@ -426,38 +313,17 @@ Default: ~,Catalog - - - - - - - - 0 - 0 - - - - - 175 - 0 - - - - - 200 - 16777215 - - - - + + + Qt::Vertical + + + + - &Description note - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + &Extra note header_note_source_field @@ -478,6 +344,12 @@ Default: ~,Catalog 0 + + + 16777215 + 16777215 + + Custom column source for note to include in Description header area @@ -485,7 +357,7 @@ Default: ~,Catalog - + diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index a7da345dc2..385a699c7b 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -48,29 +48,13 @@ class EPUB_MOBI(CatalogPlugin): "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), Option('--exclude-genre', - default='\[.+\]', + default='\[.+\]|\+', dest='exclude_genre', action = None, - help=_("Regex describing tags to exclude as genres.\n" "Default: '%default' excludes bracketed tags, e.g. '[]'\n" + help=_("Regex describing tags to exclude as genres.\n" + "Default: '%default' excludes bracketed tags, e.g. '[Project Gutenberg]', and '+', the default tag for read books.\n" "Applies to: ePub, MOBI output formats")), -# Option('--exclude-book-marker', -# default=':', -# dest='exclude_book_marker', -# action = None, -# help=_("#:pattern specifying custom field/contents indicating book should be excluded.\n" -# "For example: '#status:Archived' will exclude a book with a value of 'Archived' in the custom column 'status'.\n" -# "Default: '%default'\n" -# "Applies to ePub, MOBI output formats")), -# Option('--exclude-tags', -# default=('~,Catalog'), -# dest='exclude_tags', -# action = None, -# help=_("Comma-separated list of tag words indicating book should be excluded from output. " -# "For example: 'skip' will match 'skip this book' and 'Skip will like this'. " -# "Default:'%default'\n" -# "Applies to: ePub, MOBI output formats")), - Option('--exclusion-rules', default="(('Excluded tags','Tags','~,Catalog'),)", dest='exclusion_rules', diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 6cc94c0e08..111ceda9fe 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -657,14 +657,36 @@ Author '{0}': # Merge opts.exclude_tags with opts.search_text # Updated to use exact match syntax - empty_exclude_tags = False if len(self.opts.exclude_tags) else True + + exclude_tags = [] + for rule in self.opts.exclusion_rules: + if rule[1].lower() == 'tags': + exclude_tags.extend(rule[2].split(',')) + + # Remove dups + self.exclude_tags = exclude_tags = list(set(exclude_tags)) + + if self.opts.verbose and self.exclude_tags: + #self.opts.log.info(" excluding tag list %s" % exclude_tags) + search_terms = [] + for tag in exclude_tags: + search_terms.append("tag:=%s" % tag) + search_phrase = "%s" % " or ".join(search_terms) + self.opts.search_text = search_phrase + data = self.plugin.search_sort_db(self.db, self.opts) + for record in data: + self.opts.log.info("\t- %s (Exclusion rule %s)" % (record['title'], exclude_tags)) + # Reset the database + self.opts.search_text = '' + data = self.plugin.search_sort_db(self.db, self.opts) + search_phrase = '' - if not empty_exclude_tags: - exclude_tags = self.opts.exclude_tags.split(',') + if exclude_tags: search_terms = [] for tag in exclude_tags: search_terms.append("tag:=%s" % tag) search_phrase = "not (%s)" % " or ".join(search_terms) + # If a list of ids are provided, don't use search_text if self.opts.ids: self.opts.search_text = search_phrase @@ -1672,14 +1694,13 @@ Author '{0}': self.opts.sort_by = 'series' - # Merge opts.exclude_tags with opts.search_text + # Merge self.exclude_tags with opts.search_text # Updated to use exact match syntax - empty_exclude_tags = False if len(self.opts.exclude_tags) else True + search_phrase = 'series:true ' - if not empty_exclude_tags: - exclude_tags = self.opts.exclude_tags.split(',') + if self.exclude_tags: search_terms = [] - for tag in exclude_tags: + for tag in self.exclude_tags: search_terms.append("tag:=%s" % tag) search_phrase += "not (%s)" % " or ".join(search_terms) @@ -3120,7 +3141,7 @@ Author '{0}': Evaluate conditions for including prefixes in various listings ''' def log_prefix_rule_match_info(rule, record): - self.opts.log.info(" %s %s by %s (Prefix rule '%s': %s:%s)" % + self.opts.log.info("\t%s %s by %s (Prefix rule '%s': %s:%s)" % (rule['prefix'],record['title'], record['authors'][0], rule['name'], rule['field'],rule['pattern'])) @@ -3816,9 +3837,14 @@ Author '{0}': return friendly_tag def getMarkerTags(self): - ''' Return a list of special marker tags to be excluded from genre list ''' + ''' + Return a list of special marker tags to be excluded from genre list + exclusion_rules = ('name','Tags|#column','[]|pattern') + ''' markerTags = [] - markerTags.extend(self.opts.exclude_tags.split(',')) + for rule in self.opts.exclusion_rules: + if rule[1].lower() == 'tags': + markerTags.extend(rule[2].split(',')) return markerTags def letter_or_symbol(self,char): @@ -3996,21 +4022,40 @@ Author '{0}': ''' Remove excluded entries ''' - field, pat = self.opts.exclude_book_marker.split(':') - if pat == '': - return data_set filtered_data_set = [] - for record in data_set: - field_contents = self.__db.get_field(record['id'], - field, - index_is_id=True) - if field_contents: - if re.search(pat, unicode(field_contents), - re.IGNORECASE) is not None: - continue - filtered_data_set.append(record) + exclusion_pairs = [] + exclusion_set = [] + for rule in self.opts.exclusion_rules: + if rule[1].startswith('#') and rule[2] != '': + field = rule[1] + pat = rule[2] + exclusion_pairs.append((field,pat)) + else: + continue - return filtered_data_set + if exclusion_pairs: + for record in data_set: + for exclusion_pair in exclusion_pairs: + field,pat = exclusion_pair + field_contents = self.__db.get_field(record['id'], + field, + index_is_id=True) + if field_contents: + if re.search(pat, unicode(field_contents), + re.IGNORECASE) is not None: + if self.opts.verbose: + self.opts.log.info(" excluding '%s' (%s:%s)" % (record['title'], field, pat)) + exclusion_set.append(record) + if record in filtered_data_set: + filtered_data_set.remove(record) + break + else: + if (record not in filtered_data_set and + record not in exclusion_set): + filtered_data_set.append(record) + return filtered_data_set + else: + return data_set def processSpecialTags(self, tags, this_title, opts): From e40bd0164dbf84ab0ceb3f6211591b08cd89687a Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 6 Aug 2012 10:52:07 -0600 Subject: [PATCH 06/11] Removed table selection when lost focus, added reset button to exclude_genres --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 124 +++++++----------- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 13 +- .../library/catalogs/epub_mobi_builder.py | 18 +-- 3 files changed, 69 insertions(+), 86 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 8a2035c76f..57a23211e3 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -18,7 +18,7 @@ from PyQt4.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QHBoxLayout, QIcon, QLabel, QLineEdit, QPlainTextEdit, QRadioButton, QSize, QSizePolicy, - QTableWidget, QTableWidgetItem, + QTableWidget, QTableWidgetItem, QTimer, QToolButton, QVBoxLayout, QWidget) class PluginWidget(QWidget,Ui_Form): @@ -72,8 +72,6 @@ class PluginWidget(QWidget,Ui_Form): # LineEditControls option_fields += zip(['exclude_genre'],['\[.+\]|\+'],['line_edit']) - #***option_fields += zip(['exclude_pattern'],[None],['line_edit']) - #***option_fields += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit']) # SpinBoxControls option_fields += zip(['thumb_width'],[1.00],['spin_box']) @@ -84,12 +82,7 @@ class PluginWidget(QWidget,Ui_Form): 'enabled':True, 'name':'Catalogs', 'field':'Tags', - 'pattern':'Catalog'}, - {'ordinal':1, - 'enabled':False, - 'name':'New rule', - 'field':'', - 'pattern':''}], + 'pattern':'Catalog'},], ['table_widget','table_widget']) # Prefix rules @@ -105,13 +98,7 @@ class PluginWidget(QWidget,Ui_Form): 'name':'Wishlist item', 'field':'Tags', 'pattern':'Wishlist', - 'prefix':u'\u00d7'}, - {'ordinal':2, - 'enabled':False, - 'name':'New rule', - 'field':'', - 'pattern':'', - 'prefix':''}], + 'prefix':u'\u00d7'},], ['table_widget','table_widget','table_widget']) self.OPTION_FIELDS = option_fields @@ -181,15 +168,9 @@ class PluginWidget(QWidget,Ui_Form): if opt_value not in prefix_rules: prefix_rules.append(opt_value) - ''' - *** - # Init self.exclude_source_field_name - self.exclude_source_field_name = '' - cs = unicode(self.exclude_source_field.currentText()) - if cs > '': - exclude_source_spec = self.exclude_source_fields[cs] - self.exclude_source_field_name = exclude_source_spec['field'] - ''' + # Add icon to the reset button + self.reset_exclude_genres_tb.setIcon(QIcon(I('trash.png'))) + self.reset_exclude_genres_tb.clicked.connect(self.reset_exclude_genres) # Init self.merge_source_field_name self.merge_source_field_name = '' @@ -205,12 +186,6 @@ class PluginWidget(QWidget,Ui_Form): header_note_source_spec = self.header_note_source_fields[cs] self.header_note_source_field_name = header_note_source_spec['field'] - # Hook changes to thumb_width - self.thumb_width.valueChanged.connect(self.thumb_width_changed) - - # Hook changes to Description section - self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) - # Initialize exclusion rules self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb_hl, "exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db) @@ -219,6 +194,13 @@ class PluginWidget(QWidget,Ui_Form): self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, "prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db) + # Hook changes to thumb_width + self.thumb_width.valueChanged.connect(self.thumb_width_changed) + + # Hook changes to Description section + self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) + + def options(self): # Save/return the current options # exclude_genre stores literally @@ -310,15 +292,6 @@ class PluginWidget(QWidget,Ui_Form): else: opts_dict[c_name] = opt_value - ''' - *** - # Generate markers for hybrids - #opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field_name, - # self.read_pattern.text()) - opts_dict['exclude_book_marker'] = "%s:%s" % (self.exclude_source_field_name, - self.exclude_pattern.text()) - ''' - # Generate specs for merge_comments, header_note_source_field checked = '' if self.merge_before.isChecked(): @@ -365,17 +338,6 @@ class PluginWidget(QWidget,Ui_Form): if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']: custom_fields[field_md['name']] = {'field':custom_field, 'datatype':field_md['datatype']} - ''' - *** - # Blank field first - self.exclude_source_field.addItem('') - # Add the sorted eligible fields to the combo box - for cf in sorted(custom_fields, key=sort_key): - self.exclude_source_field.addItem(cf) - self.exclude_source_fields = custom_fields - self.exclude_source_field.currentIndexChanged.connect(self.exclude_source_field_changed) - ''' - # Populate the 'Header note' combo box custom_fields = {} for custom_field in self.all_custom_fields: @@ -508,6 +470,12 @@ 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 thumb_width_changed(self,new_value): ''' Process changes in the thumb_width spin box @@ -581,19 +549,20 @@ class GenericRulesTable(QTableWidget): self.layout = parent_gb_hl # Add ourselves to the layout - sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) + #print("verticalHeader: %s" % dir(self.verticalHeader())) + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + #sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) - self.setMaximumSize(QSize(16777215, 114)) + self.setMaximumSize(QSize(16777215, 113)) + self.setColumnCount(0) self.setRowCount(0) + self.layout.addWidget(self) - self._init_table_widget() self._init_controls() - self._initialize() def _init_controls(self): # Add the control set @@ -628,18 +597,6 @@ class GenericRulesTable(QTableWidget): self.layout.addLayout(vbl) - def _init_table_widget(self): - ''' - Override this in the specific instance - ''' - pass - - def _initialize(self): - ''' - Override this in the specific instance - ''' - pass - def add_row(self): self.setFocus() row = self.currentRow() + 1 @@ -683,6 +640,10 @@ class GenericRulesTable(QTableWidget): def get_data(self): pass + def focusOutEvent(self,e): + # Override of QTableWidget method + self.clearSelection() + def move_row_down(self): self.setFocus() rows = self.selectionModel().selectedRows() @@ -760,8 +721,21 @@ class GenericRulesTable(QTableWidget): self.selectRow(row) self.scrollToItem(self.currentItem()) + def tweak_height(self, height=4): + for i in range(min(3,self.rowCount())): + height += self.rowHeight(i) + height += self.verticalHeader().sizeHint().height() + print("computed table height for %d rows: %d" % (self.rowCount(),height, )) + self.setMinimumSize(QSize(16777215, height)) + self.setMaximumSize(QSize(16777215, height)) + class ExclusionRules(GenericRulesTable): + def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): + super(ExclusionRules, self).__init__(parent_gb_hl, object_name, rules, eligible_custom_fields, db) + self._init_table_widget() + self._initialize() + def _init_table_widget(self): header_labels = ['','Name','Field','Value'] self.setColumnCount(len(header_labels)) @@ -770,13 +744,11 @@ class ExclusionRules(GenericRulesTable): self.setSelectionBehavior(QAbstractItemView.SelectRows) def _initialize(self): - # Override max size (118) set in GenericRulesTable - self.setMaximumSize(QSize(16777215, 83)) - self.populate() self.resizeColumnsToContents() self.resize_name(1.5) self.horizontalHeader().setStretchLastSection(True) + self.clearSelection() def convert_row_to_data(self, row): data = self.create_blank_row_data() @@ -790,7 +762,7 @@ class ExclusionRules(GenericRulesTable): def create_blank_row_data(self): data = {} data['ordinal'] = -1 - data['enabled'] = False + data['enabled'] = True data['name'] = 'New rule' data['field'] = '' data['pattern'] = '' @@ -876,6 +848,11 @@ class ExclusionRules(GenericRulesTable): class PrefixRules(GenericRulesTable): + def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): + super(PrefixRules, self).__init__(parent_gb_hl, object_name, rules, eligible_custom_fields, db) + self._init_table_widget() + self._initialize() + def _init_table_widget(self): header_labels = ['','Name','Prefix','Field','Value'] self.setColumnCount(len(header_labels)) @@ -889,6 +866,7 @@ class PrefixRules(GenericRulesTable): self.resizeColumnsToContents() self.resize_name(1.5) self.horizontalHeader().setStretchLastSection(True) + self.clearSelection() def convert_row_to_data(self, row): data = self.create_blank_row_data() @@ -903,7 +881,7 @@ class PrefixRules(GenericRulesTable): def create_blank_row_data(self): data = {} data['ordinal'] = -1 - data['enabled'] = False + data['enabled'] = True data['name'] = 'New rule' data['field'] = '' data['pattern'] = '' diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 29ca39bf10..784806c15e 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -111,7 +111,8 @@ <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books.</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html> Excluded genres @@ -178,6 +179,16 @@ p, li { white-space: pre-wrap; } + + + + Reset to default + + + ... + + + diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 111ceda9fe..63fe02d5f1 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -666,19 +666,13 @@ Author '{0}': # Remove dups self.exclude_tags = exclude_tags = list(set(exclude_tags)) + # Report tag exclusions if self.opts.verbose and self.exclude_tags: - #self.opts.log.info(" excluding tag list %s" % exclude_tags) - search_terms = [] - for tag in exclude_tags: - search_terms.append("tag:=%s" % tag) - search_phrase = "%s" % " or ".join(search_terms) - self.opts.search_text = search_phrase - data = self.plugin.search_sort_db(self.db, self.opts) + data = self.db.get_data_as_dict(ids=self.opts.ids) for record in data: - self.opts.log.info("\t- %s (Exclusion rule %s)" % (record['title'], exclude_tags)) - # Reset the database - self.opts.search_text = '' - data = self.plugin.search_sort_db(self.db, self.opts) + matched = list(set(record['tags']) & set(exclude_tags)) + if matched : + self.opts.log.info(" - %s (Exclusion rule %s)" % (record['title'], matched)) search_phrase = '' if exclude_tags: @@ -3141,7 +3135,7 @@ Author '{0}': Evaluate conditions for including prefixes in various listings ''' def log_prefix_rule_match_info(rule, record): - self.opts.log.info("\t%s %s by %s (Prefix rule '%s': %s:%s)" % + self.opts.log.info(" %s %s by %s (Prefix rule '%s': %s:%s)" % (rule['prefix'],record['title'], record['authors'][0], rule['name'], rule['field'],rule['pattern'])) From 1f2e84181f9d77efaed4152a77b89b188909621d Mon Sep 17 00:00:00 2001 From: GRiker Date: Mon, 6 Aug 2012 17:34:47 -0600 Subject: [PATCH 07/11] Improved focus handling for tables. Removed obsolete characters in profiles.py. Revised tool tips. --- src/calibre/customize/profiles.py | 8 - src/calibre/gui2/catalog/catalog_epub_mobi.py | 205 ++++++------------ src/calibre/gui2/catalog/catalog_epub_mobi.ui | 36 +-- .../library/catalogs/epub_mobi_builder.py | 7 +- 4 files changed, 84 insertions(+), 172 deletions(-) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 84db12e161..78b61d4345 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -251,10 +251,8 @@ class OutputProfile(Plugin): periodical_date_in_title = True #: Characters used in jackets and catalogs - missing_char = u'x' ratings_char = u'*' empty_ratings_char = u' ' - read_char = u'+' #: Unsupported unicode characters to be replaced during preprocessing unsupported_unicode_chars = [] @@ -292,10 +290,8 @@ class iPadOutput(OutputProfile): } ] - missing_char = u'\u2715\u200a' # stylized 'x' plus hair space ratings_char = u'\u2605' # filled star empty_ratings_char = u'\u2606' # hollow star - read_char = u'\u2713' # check mark touchscreen = True # touchscreen_news_css {{{ @@ -626,10 +622,8 @@ class KindleOutput(OutputProfile): supports_mobi_indexing = True periodical_date_in_title = False - missing_char = u'x\u2009' empty_ratings_char = u'\u2606' ratings_char = u'\u2605' - read_char = u'\u2713' mobi_ems_per_blockquote = 2.0 @@ -651,10 +645,8 @@ class KindleDXOutput(OutputProfile): #comic_screen_size = (741, 1022) supports_mobi_indexing = True periodical_date_in_title = False - missing_char = u'x\u2009' empty_ratings_char = u'\u2606' ratings_char = u'\u2605' - read_char = u'\u2713' mobi_ems_per_blockquote = 2.0 @classmethod diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index 57a23211e3..485f84a642 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -6,6 +6,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' +from copy import copy from functools import partial from calibre.ebooks.conversion.config import load_defaults @@ -138,7 +139,6 @@ class PluginWidget(QWidget,Ui_Form): self.fetchEligibleCustomFields() self.populate_combo_boxes() - # Update dialog fields from stored options exclusion_rules = [] prefix_rules = [] @@ -161,12 +161,13 @@ class PluginWidget(QWidget,Ui_Form): getattr(self, c_name).setChecked(opt_value) elif c_type in ['spin_box']: getattr(self, c_name).setValue(float(opt_value)) - elif c_type in ['table_widget'] and c_name == 'exclusion_rules_tw': - if opt_value not in exclusion_rules: - exclusion_rules.append(opt_value) - elif c_type in ['table_widget'] and c_name == 'prefix_rules_tw': - if opt_value not in prefix_rules: - prefix_rules.append(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) # Add icon to the reset button self.reset_exclude_genres_tb.setIcon(QIcon(I('trash.png'))) @@ -187,20 +188,13 @@ class PluginWidget(QWidget,Ui_Form): self.header_note_source_field_name = header_note_source_spec['field'] # Initialize exclusion rules - self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb_hl, + self.exclusion_rules_table = ExclusionRules(self.exclusion_rules_gb, "exclusion_rules_tw",exclusion_rules, self.eligible_custom_fields,self.db) # Initialize prefix rules - self.prefix_rules_table = PrefixRules(self.prefix_rules_gb_hl, + self.prefix_rules_table = PrefixRules(self.prefix_rules_gb, "prefix_rules_tw",prefix_rules, self.eligible_custom_fields,self.db) - # Hook changes to thumb_width - self.thumb_width.valueChanged.connect(self.thumb_width_changed) - - # Hook changes to Description section - self.generate_descriptions.stateChanged.connect(self.generate_descriptions_changed) - - def options(self): # Save/return the current options # exclude_genre stores literally @@ -214,6 +208,11 @@ class PluginWidget(QWidget,Ui_Form): 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']: @@ -225,22 +224,21 @@ class PluginWidget(QWidget,Ui_Form): 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 + # Store UI values to gui.json in config dir gprefs.set(self.name + '_' + c_name, opt_value) # Construct opts object for catalog builder - if c_name == 'exclude_tags': - # store as list - opts_dict[c_name] = opt_value.split(',') - elif c_name == 'prefix_rules_tw': - if prefix_rules_processed: - continue + if c_name == 'prefix_rules_tw': rule_set = [] - for rule in opt_value: + for stored_rule in opt_value: # Test for empty name/field/pattern/prefix, continue # If pattern = any or unspecified, convert to regex + rule = copy(stored_rule) if not rule['enabled']: continue elif not rule['field'] or not rule['pattern'] or not rule['prefix']: @@ -250,24 +248,22 @@ class PluginWidget(QWidget,Ui_Form): # Look up custom column name #print(self.eligible_custom_fields[rule['field']]['field']) rule['field'] = self.eligible_custom_fields[rule['field']]['field'] - if rule['pattern'].startswith('any'): - rule['pattern'] = '.*' - elif rule['pattern'] == 'unspecified': - rule['pattern'] = 'None' + if rule['pattern'].startswith('any'): + rule['pattern'] = '.*' + elif rule['pattern'] == 'unspecified': + rule['pattern'] = 'None' - pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix']) - rule_set.append(pr) + pr = (rule['name'],rule['field'],rule['pattern'],rule['prefix']) + rule_set.append(pr) opt_value = tuple(rule_set) opts_dict['prefix_rules'] = opt_value - prefix_rules_processed = True elif c_name == 'exclusion_rules_tw': - if exclusion_rules_processed: - continue rule_set = [] - for rule in opt_value: + for stored_rule in opt_value: # Test for empty name/field/pattern/prefix, continue # If pattern = any or unspecified, convert to regex + rule = copy(stored_rule) if not rule['enabled']: continue elif not rule['field'] or not rule['pattern']: @@ -277,17 +273,15 @@ class PluginWidget(QWidget,Ui_Form): # Look up custom column name #print(self.eligible_custom_fields[rule['field']]['field']) rule['field'] = self.eligible_custom_fields[rule['field']]['field'] - if rule['pattern'].startswith('any'): - rule['pattern'] = '.*' - elif rule['pattern'] == 'unspecified': - rule['pattern'] = 'None' - - pr = (rule['name'],rule['field'],rule['pattern']) - rule_set.append(pr) + if rule['pattern'].startswith('any'): + rule['pattern'] = '.*' + elif rule['pattern'] == 'unspecified': + rule['pattern'] = 'None' + pr = (rule['name'],rule['field'],rule['pattern']) + rule_set.append(pr) opt_value = tuple(rule_set) opts_dict['exclusion_rules'] = opt_value - exclusion_rules_processed = True else: opts_dict[c_name] = opt_value @@ -372,74 +366,6 @@ class PluginWidget(QWidget,Ui_Form): self.merge_after.setEnabled(False) self.include_hr.setEnabled(False) - def read_source_field_changed(self,new_index): - ''' - Process changes in the read_source_field combo box - Currently using QLineEdit for all field types - Possible to modify to switch QWidget type - ''' - new_source = unicode(self.read_source_field.currentText()) - read_source_spec = self.read_source_fields[new_source] - self.read_source_field_name = read_source_spec['field'] - - # Change pattern input widget to match the source field datatype - if read_source_spec['datatype'] in ['bool','composite','datetime','text']: - if not isinstance(self.read_pattern, QLineEdit): - self.read_spec_hl.removeWidget(self.read_pattern) - dw = QLineEdit(self) - dw.setObjectName('read_pattern') - dw.setToolTip('Pattern for read book') - self.read_pattern = dw - self.read_spec_hl.addWidget(dw) - - def exclude_source_field_changed(self,new_index): - ''' - Process changes in the exclude_source_field combo box - Currently using QLineEdit for all field types - Possible to modify to switch QWidget type - ''' - new_source = str(self.exclude_source_field.currentText()) - self.exclude_source_field_name = new_source - if new_source > '': - exclude_source_spec = self.exclude_source_fields[unicode(new_source)] - self.exclude_source_field_name = exclude_source_spec['field'] - self.exclude_pattern.setEnabled(True) - - # Change pattern input widget to match the source field datatype - if exclude_source_spec['datatype'] in ['bool','composite','datetime','text']: - if not isinstance(self.exclude_pattern, QLineEdit): - self.exclude_spec_hl.removeWidget(self.exclude_pattern) - dw = QLineEdit(self) - dw.setObjectName('exclude_pattern') - dw.setToolTip('Exclusion pattern') - self.exclude_pattern = dw - self.exclude_spec_hl.addWidget(dw) - else: - self.exclude_pattern.setEnabled(False) - - def generate_descriptions_changed(self,new_state): - ''' - Process changes to Descriptions section - 0: unchecked - 2: checked - ''' - - return - ''' - if new_state == 0: - # unchecked - self.merge_source_field.setEnabled(False) - self.merge_before.setEnabled(False) - self.merge_after.setEnabled(False) - self.include_hr.setEnabled(False) - elif new_state == 2: - # checked - self.merge_source_field.setEnabled(True) - self.merge_before.setEnabled(True) - self.merge_after.setEnabled(True) - self.include_hr.setEnabled(True) - ''' - def header_note_source_field_changed(self,new_index): ''' Process changes in the header_note_source_field combo box @@ -476,12 +402,6 @@ class PluginWidget(QWidget,Ui_Form): self.exclude_genre.setText(default[1]) break - def thumb_width_changed(self,new_value): - ''' - Process changes in the thumb_width spin box - ''' - pass - class CheckableTableWidgetItem(QTableWidgetItem): ''' @@ -540,13 +460,14 @@ class GenericRulesTable(QTableWidget): placeholders for basic methods to be overriden ''' - def __init__(self, parent_gb_hl, object_name, rules, eligible_custom_fields, db): + def __init__(self, parent_gb, object_name, rules, eligible_custom_fields, db): self.rules = rules self.eligible_custom_fields = eligible_custom_fields self.db = db QTableWidget.__init__(self) self.setObjectName(object_name) - self.layout = parent_gb_hl + self.layout = QHBoxLayout() + parent_gb.setLayout(self.layout) # Add ourselves to the layout #print("verticalHeader: %s" % dir(self.verticalHeader())) @@ -559,9 +480,11 @@ class GenericRulesTable(QTableWidget): self.setColumnCount(0) self.setRowCount(0) - self.layout.addWidget(self) + self.last_row_selected = self.currentRow() + self.last_rows_selected = self.selectionModel().selectedRows() + self._init_controls() def _init_controls(self): @@ -599,12 +522,12 @@ class GenericRulesTable(QTableWidget): def add_row(self): self.setFocus() - row = self.currentRow() + 1 + row = self.last_row_selected + 1 self.insertRow(row) self.populate_table_row(row, self.create_blank_row_data()) self.select_and_scroll_to_row(row) self.resizeColumnsToContents() - # Just in case table was empty + # In case table was empty self.horizontalHeader().setStretchLastSection(True) def convert_row_to_data(self): @@ -621,12 +544,16 @@ class GenericRulesTable(QTableWidget): def delete_row(self): self.setFocus() - rows = self.selectionModel().selectedRows() + rows = self.last_rows_selected if len(rows) == 0: return - message = '

Are you sure you want to delete this rule?' + + first = rows[0].row() + 1 + last = rows[-1].row() + 1 + + message = '

Are you sure you want to delete rule %d?' % first if len(rows) > 1: - message = '

Are you sure you want to delete the %d selected rules?'%len(rows) + message = '

Are you sure you want to delete rules %d-%d?' % (first, last) if not question_dialog(self, _('Are you sure?'), message, show_copy_button=False): return first_sel_row = self.currentRow() @@ -637,16 +564,18 @@ class GenericRulesTable(QTableWidget): elif self.rowCount() > 0: self.select_and_scroll_to_row(first_sel_row - 1) + def focusOutEvent(self,e): + # Override of QTableWidget method - clear selection when table loses focus + self.last_row_selected = self.currentRow() + self.last_rows_selected = self.selectionModel().selectedRows() + self.clearSelection() + def get_data(self): pass - def focusOutEvent(self,e): - # Override of QTableWidget method - self.clearSelection() - def move_row_down(self): self.setFocus() - rows = self.selectionModel().selectedRows() + rows = self.last_rows_selected if len(rows) == 0: return last_sel_row = rows[-1].row() @@ -673,11 +602,11 @@ class GenericRulesTable(QTableWidget): scroll_to_row = last_sel_row + 1 if scroll_to_row < self.rowCount() - 1: scroll_to_row = scroll_to_row + 1 - self.scrollToItem(self.item(scroll_to_row, 0)) + self.select_and_scroll_to_row(scroll_to_row) def move_row_up(self): self.setFocus() - rows = self.selectionModel().selectedRows() + rows = self.last_rows_selected if len(rows) == 0: return first_sel_row = rows[0].row() @@ -699,7 +628,7 @@ class GenericRulesTable(QTableWidget): scroll_to_row = first_sel_row - 1 if scroll_to_row > 0: scroll_to_row = scroll_to_row - 1 - self.scrollToItem(self.item(scroll_to_row, 0)) + self.select_and_scroll_to_row(scroll_to_row) def populate_table_row(self): ''' @@ -744,7 +673,7 @@ class ExclusionRules(GenericRulesTable): self.setSelectionBehavior(QAbstractItemView.SelectRows) def _initialize(self): - self.populate() + self.populate_table() self.resizeColumnsToContents() self.resize_name(1.5) self.horizontalHeader().setStretchLastSection(True) @@ -780,7 +709,7 @@ class ExclusionRules(GenericRulesTable): 'pattern':data['pattern']}) return data_items - def populate(self): + def populate_table(self): # Format of rules list is different if default values vs retrieved JSON # Hack to normalize list style rules = self.rules @@ -842,6 +771,8 @@ class ExclusionRules(GenericRulesTable): values = ['True','False','unspecified'] elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: values = ['any value','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: + values = ['any date','unspecified'] values_combo = ComboBox(self, values, pattern) self.setCellWidget(row, 3, values_combo) @@ -862,7 +793,7 @@ class PrefixRules(GenericRulesTable): def _initialize(self): self.generate_prefix_list() - self.populate() + self.populate_table() self.resizeColumnsToContents() self.resize_name(1.5) self.horizontalHeader().setStretchLastSection(True) @@ -1036,7 +967,7 @@ class PrefixRules(GenericRulesTable): 'prefix':data['prefix']}) return data_items - def populate(self): + def populate_table(self): # Format of rules list is different if default values vs retrieved JSON # Hack to normalize list style rules = self.rules @@ -1108,10 +1039,10 @@ class PrefixRules(GenericRulesTable): values = sorted(values, key=sort_key) elif self.eligible_custom_fields[source_field]['datatype'] in ['bool']: values = ['True','False','unspecified'] - elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: - values = ['any date','unspecified'] elif self.eligible_custom_fields[source_field]['datatype'] in ['composite']: values = ['any value','unspecified'] + elif self.eligible_custom_fields[source_field]['datatype'] in ['datetime']: + values = ['any date','unspecified'] values_combo = ComboBox(self, values, pattern) self.setCellWidget(row, 4, values_combo) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 784806c15e..bfe94a389f 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -35,7 +35,7 @@ - Sections to include in catalog. + Enabled sections will be included in the generated catalog. Included sections @@ -107,12 +107,8 @@ - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A regular expression describing genres to be excluded from the generated catalog. Genres are derived from the tags applied to your books.</p> -<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The default pattern \[.+\]|\+ excludes tags of the form [tag], e.g., [Project Gutenberg], and '+', the default tag for a read book.</p></body></html> + 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. Excluded genres @@ -209,16 +205,11 @@ p, li { white-space: pre-wrap; } - Matching books will not be included in generated catalog. + Books matching any of the exclusion rules will be excluded from the generated catalog. Excluded books - - - - - @@ -230,16 +221,11 @@ p, li { white-space: pre-wrap; } - The earliest enabled matching rule will be used to add a prefix to book listings in the generated catalog. + The first enabled matching rule will be used to add a prefix to book listings in the generated catalog. Prefix rules - - - - - @@ -305,7 +291,7 @@ p, li { white-space: pre-wrap; } - Size hint for Description cover thumbnails + Size hint for cover thumbnails included in Descriptions section. inch @@ -362,7 +348,7 @@ p, li { white-space: pre-wrap; } - Custom column source for note to include in Description header area + Custom column source for text to include in Description section. @@ -404,7 +390,7 @@ p, li { white-space: pre-wrap; } - Additional content merged with Comments during catalog generation + Custom column containing additional content to be merged with Comments metadata. @@ -418,7 +404,7 @@ p, li { white-space: pre-wrap; } - Merge additional content before Comments + Merge additional content before Comments metadata. &Before @@ -428,7 +414,7 @@ p, li { white-space: pre-wrap; } - Merge additional content after Comments + Merge additional content after Comments metadata. &After @@ -445,7 +431,7 @@ p, li { white-space: pre-wrap; } - Separate Comments and additional content with horizontal rule + Separate Comments metadata and additional content with a horizontal rule. &Separator diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 63fe02d5f1..825778f0d5 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -470,6 +470,7 @@ class CatalogBuilder(object): return self.__output_profile.empty_ratings_char return property(fget=fget) @dynamic_property + def READ_PROGRESS_SYMBOL(self): def fget(self): return "▪" if self.generateForKindle else '+' @@ -672,7 +673,7 @@ Author '{0}': for record in data: matched = list(set(record['tags']) & set(exclude_tags)) if matched : - self.opts.log.info(" - %s (Exclusion rule %s)" % (record['title'], matched)) + self.opts.log.info(" - %s (Exclusion rule Tags: '%s')" % (record['title'], str(matched[0]))) search_phrase = '' if exclude_tags: @@ -4038,7 +4039,9 @@ Author '{0}': if re.search(pat, unicode(field_contents), re.IGNORECASE) is not None: if self.opts.verbose: - self.opts.log.info(" excluding '%s' (%s:%s)" % (record['title'], field, pat)) + field_md = self.db.metadata_for_field(field) + self.opts.log.info(" - %s (Exclusion rule '%s': %s:%s)" % + (record['title'], field_md['name'], field,pat)) exclusion_set.append(record) if record in filtered_data_set: filtered_data_set.remove(record) From b171273f9c508332b51b72d3b1df4eee332223d3 Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 6 Aug 2012 20:19:20 -0400 Subject: [PATCH 08/11] Update Qqick start guide. --- resources/quick_start.epub | Bin 130585 -> 130580 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/quick_start.epub b/resources/quick_start.epub index 6f8f7046cd8d6cfc55ad220c6e0c381fab201f5b..a3f74213a6a374ebb271a570214af6962283c8c4 100644 GIT binary patch delta 734 zcmbRFhkeQ)_6;wj_^RfzIUfA>x9uAv14A|QqnauEHUwF$k=QZ2au+Y$_N7$1l-k4adu^^Lw%k>+R&$8^|zP0LS`&^m&`{nDz zp5NZoP+hWhTiS_58xAx0rcE_Ftr&fI(cd0N-%A>i5zWqNI@bNng=UrW&#c$iS5QB{ ze~*-Zy!wG#bLwBI&hdXHT>s-w*1s~YK#9V-kA18RI_j&~=H#2rF1XCSHu}>~_kDM{ zT`tc1^g&iDNQaNb=q%$)=|l zSIF;K;a9RtNA5k_hhxv!(jxm(yZk=9nx9ZV(JFrZae>aYWq$g_P4Dk7xZP>x|E>DU zo4fzZO|DALk`i6h>%=b9VpI0Z!h^egMb65Cg}kfM7WB=TrS4f>b9Y{EjR~LX%j%XR z)0Uc=_H@kgyZ$8JyLy^;`(mf{0#%9Z>wHpWE2q259!!<_H*Ld%-~UYhelnJ3w`U0O zW@Zs#nEwA4qr~Lv0_x3KvfHy{8BOmh$}x*DFmP}%pr#lG20aD{V4Qy77h@umg6w2R zS%c}{e=-V8PyWp)#gu0IZ{sP%1ejxVbIzRU5)qfea7++0a_LtF_`R_Nz>GIzh#l-@=S=m6g3j?7f MkoELCFc~ud0NQdx>Hq)$ delta 696 zcmV;p0!RIn{0Eu*2e9ZG4zZi#KD0aHcl!YV0Eq*W(GV?vFfMp#bZu;nQ`>ISKoEWR zS4{V@!XCR35(yiZ)Ty{AltNMgs;X*jZ{t<$U28pQ5+Oc_-{8ww+bNd_D*WKf>^W!7 zT+C0mRSEafHp=KRi-M4W)S1bZUXIzC?)+yA8WK7erO|TCv|*>Q$B(A(lEvrCb12C1 zFR#vKuM=Q@KH|qgf;e4F;nSr>EcWo2 zA6+NY$zpPSjGhC&sKrvk7pR0?D($C+$W$YE8id_d)ANTTGWDPFua1t#H#f0Uav6K4 zu5|2c_jqdZ9b~0wT)(ou0jx7uLvb>=q$2SJF=%^nL$4~7YmRq2}C-{M~w-G-&jlqmgq zA*`H~Y9+yO;Y!!WlC!b*!%@V+Z{>4rr`1gb&uwt*{evGJw;vq=d)=3s`2h?Sv^(N= z`vCv|i30!tE&u=k000000GGS^0bv3q9g}ehDVIn40Wkt|G?Q@&DVL-B0Wku#Ig@b- zD3|{G0Wtw;mofYSTLKP2lW_?tlTJYgm%jV~Dgus3lW_?tlg>y7mm2*6Dgrr9lW_?t emv8+6F#+$Fq5T0v0s{A!=KTRE2I}_#0001Ms799n From d703ef3ebef98392903bfaf0b2395d71db4aabb1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Aug 2012 09:22:32 +0530 Subject: [PATCH 09/11] Fix #1033624 (won't reconize HTC one s) --- src/calibre/devices/android/driver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 36dbcfafb7..12e7f40301 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -43,6 +43,7 @@ class ANDROID(USBMS): 0xccf : HTC_BCDS, 0xcd6 : HTC_BCDS, 0xce5 : HTC_BCDS, + 0xcec : HTC_BCDS, 0x2910 : HTC_BCDS, 0xff9 : HTC_BCDS, }, From 08f38df9b9b3f63ba6c652ce09fb184154210e8f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Aug 2012 10:41:48 +0530 Subject: [PATCH 10/11] MOBI Output: Fix ToC at start option having no effect when converting some input documents that have an out-of-spine ToC. Fixes #1033656 (Table of Contents not in Front after converting) --- src/calibre/ebooks/oeb/transforms/htmltoc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/oeb/transforms/htmltoc.py b/src/calibre/ebooks/oeb/transforms/htmltoc.py index 26b2ccf41c..0e1b2b5f10 100644 --- a/src/calibre/ebooks/oeb/transforms/htmltoc.py +++ b/src/calibre/ebooks/oeb/transforms/htmltoc.py @@ -73,7 +73,10 @@ class HTMLTOCAdder(object): if (hasattr(item.data, 'xpath') and XPath('//h:a[@href]')(item.data)): if oeb.spine.index(item) < 0: - oeb.spine.add(item, linear=False) + if self.position == 'end': + oeb.spine.add(item, linear=False) + else: + oeb.spine.insert(0, item, linear=True) return elif has_toc: oeb.guide.remove('toc') From de21068e728e912ac4e27e57fa5df5949b590c4e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Aug 2012 10:47:43 +0530 Subject: [PATCH 11/11] Fix #1033430 (line scrolling do not stop at page break in paged mode) --- src/calibre/gui2/viewer/documentview.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 18a8005ab2..0bae46d717 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -1036,14 +1036,14 @@ class DocumentView(QWebView): # {{{ if not self.handle_key_press(event): return QWebView.keyPressEvent(self, event) - def paged_col_scroll(self, forward=True): + def paged_col_scroll(self, forward=True, scroll_past_end=True): dir = 'next' if forward else 'previous' loc = self.document.javascript( 'paged_display.%s_col_location()'%dir, typ='int') if loc > -1: self.document.scroll_to(x=loc, y=0) self.manager.scrolled(self.document.scroll_fraction) - else: + elif scroll_past_end: (self.manager.next_document() if forward else self.manager.previous_document()) @@ -1059,7 +1059,8 @@ class DocumentView(QWebView): # {{{ self.is_auto_repeat_event = False elif key == 'Down': if self.document.in_paged_mode: - self.paged_col_scroll() + self.paged_col_scroll(scroll_past_end=not + self.document.line_scrolling_stops_on_pagebreaks) else: if (not self.document.line_scrolling_stops_on_pagebreaks and self.document.at_bottom): @@ -1068,7 +1069,8 @@ class DocumentView(QWebView): # {{{ self.scroll_by(y=15) elif key == 'Up': if self.document.in_paged_mode: - self.paged_col_scroll(forward=False) + self.paged_col_scroll(forward=False, scroll_past_end=not + self.document.line_scrolling_stops_on_pagebreaks) else: if (not self.document.line_scrolling_stops_on_pagebreaks and self.document.at_top):