From fa0533ec0d60558670670b97cf1987eff8c11f90 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 10 Mar 2016 16:40:16 +0530 Subject: [PATCH] Conversion: Implement style transformation via rules. Look under the "Transform Styles" tab of the Look & Feel section of the conversion dialog --- src/calibre/gui2/convert/look_and_feel.py | 12 ++- src/calibre/gui2/convert/look_and_feel.ui | 31 ++++++ src/calibre/gui2/css_transform_rules.py | 111 +++++++++++++++++++--- src/calibre/gui2/tag_mapper.py | 83 +++++++++------- 4 files changed, 187 insertions(+), 50 deletions(-) diff --git a/src/calibre/gui2/convert/look_and_feel.py b/src/calibre/gui2/convert/look_and_feel.py index 80dd7e081a..1051477582 100644 --- a/src/calibre/gui2/convert/look_and_feel.py +++ b/src/calibre/gui2/convert/look_and_feel.py @@ -6,6 +6,8 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import json + from PyQt5.Qt import Qt, QSize from calibre.gui2.convert.look_and_feel_ui import Ui_Form @@ -40,7 +42,7 @@ class LookAndFeelWidget(Widget, Ui_Form): 'insert_blank_line_size', 'input_encoding', 'filter_css', 'expand_css', 'asciiize', 'keep_ligatures', - 'linearize_tables'] + 'linearize_tables', 'transform_css_rules'] ) for val, text in [ ('original', _('Original')), @@ -80,6 +82,8 @@ class LookAndFeelWidget(Widget, Ui_Form): val = unicode(g.text()).strip() val = [x.strip() for x in val.split(',' if ',' in val else ' ') if x.strip()] return ', '.join(val) or None + if g is self.opt_transform_css_rules: + return json.dumps(g.rules) return Widget.get_value_handler(self, g) def set_value_handler(self, g, val): @@ -106,6 +110,9 @@ class LookAndFeelWidget(Widget, Ui_Form): if g is self.opt_extra_css: g.load_text(val or '', 'css') return True + if g is self.opt_transform_css_rules: + g.rules = json.loads(val) if val else [] + return True def connect_gui_obj_handler(self, gui_obj, slot): if gui_obj is self.opt_filter_css: @@ -114,6 +121,9 @@ class LookAndFeelWidget(Widget, Ui_Form): w.stateChanged.connect(slot) self.filter_css_others.textChanged.connect(slot) return + if gui_obj is self.opt_transform_css_rules: + gui_obj.changed.connect(slot) + return raise NotImplementedError() def font_key_wizard(self): diff --git a/src/calibre/gui2/convert/look_and_feel.ui b/src/calibre/gui2/convert/look_and_feel.ui index 504ed999fb..c43598c87e 100644 --- a/src/calibre/gui2/convert/look_and_feel.ui +++ b/src/calibre/gui2/convert/look_and_feel.ui @@ -475,6 +475,31 @@ + + + Transform styles + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + @@ -495,6 +520,12 @@ QPlainTextEdit
calibre/gui2/tweak_book/editor/text.h
+ + RulesWidget + QWidget +
calibre/gui2/css_transform_rules.h
+ 1 +
diff --git a/src/calibre/gui2/css_transform_rules.py b/src/calibre/gui2/css_transform_rules.py index 68b9604671..3c1c7347db 100644 --- a/src/calibre/gui2/css_transform_rules.py +++ b/src/calibre/gui2/css_transform_rules.py @@ -6,15 +6,16 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) from PyQt5.Qt import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QLineEdit, QPushButton, QSize + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QLineEdit, + QPushButton, QSize, pyqtSignal, QMenu ) from calibre.ebooks.css_transform_rules import ( - validate_rule, safe_parser, compile_rules, transform_sheet, ACTION_MAP, MATCH_TYPE_MAP) -from calibre.gui2 import error_dialog, elided_text + validate_rule, safe_parser, compile_rules, transform_sheet, ACTION_MAP, MATCH_TYPE_MAP, export_rules, import_rules) +from calibre.gui2 import error_dialog, elided_text, choose_save_file, choose_files from calibre.gui2.tag_mapper import ( RuleEditDialog as RuleEditDialogBase, Rules as RulesBase, RulesDialog as - RulesDialogBase, RuleItem as RuleItemBase) + RulesDialogBase, RuleItem as RuleItemBase, SaveLoadMixin) from calibre.gui2.widgets2 import Dialog from calibre.utils.config import JSONConfig @@ -152,14 +153,19 @@ class RuleItem(RuleItemBase): # {{{ @staticmethod def text_from_rule(rule, parent): - query = elided_text(rule['query'], font=parent.font(), width=200, pos='right') - text = _( - 'If the property {property} {match_type} {query}
{action}').format( - property=rule['property'], action=ACTION_MAP[rule['action']], - match_type=MATCH_TYPE_MAP[rule['match_type']], query=query) - if rule['action_data']: - ad = elided_text(rule['action_data'], font=parent.font(), width=200, pos='right') - text += ' %s' % ad + try: + query = elided_text(rule['query'], font=parent.font(), width=200, pos='right') + text = _( + 'If the property {property} {match_type} {query}
{action}').format( + property=rule['property'], action=ACTION_MAP[rule['action']], + match_type=MATCH_TYPE_MAP[rule['match_type']], query=query) + if rule['action_data']: + ad = elided_text(rule['action_data'], font=parent.font(), width=200, pos='right') + text += ' %s' % ad + except Exception: + import traceback + traceback.print_exc() + text = _('This rule is invalid, please remove it') return text # }}} @@ -218,7 +224,7 @@ class Tester(Dialog): # {{{ return QSize(800, 600) # }}} -class RulesDialog(RulesDialogBase): +class RulesDialog(RulesDialogBase): # {{{ DIALOG_TITLE = _('Edit style transform rules') PREFS_NAME = 'edit-style-transform-rules' @@ -230,6 +236,85 @@ class RulesDialog(RulesDialogBase): # multiple processes self.PREFS_OBJECT = JSONConfig('style-transform-rules') RulesDialogBase.__init__(self, *args, **kw) +# }}} + +class RulesWidget(QWidget, SaveLoadMixin): # {{{ + + changed = pyqtSignal() + + def __init__(self, parent=None): + self.loaded_ruleset = None + QWidget.__init__(self, parent) + self.PREFS_OBJECT = JSONConfig('style-transform-rules') + l = QVBoxLayout(self) + self.rules_widget = w = Rules(self) + w.changed.connect(self.changed.emit) + l.addWidget(w) + self.h = h = QHBoxLayout() + l.addLayout(h) + self.export_button = b = QPushButton(_('E&xport'), self) + b.setToolTip(_('Export these rules to a file')) + b.clicked.connect(self.export_rules) + h.addWidget(b) + self.import_button = b = QPushButton(_('&Import'), self) + b.setToolTip(_('Import previously exported rules')) + b.clicked.connect(self.import_rules) + h.addWidget(b) + self.test_button = b = QPushButton(_('&Test rules'), self) + b.clicked.connect(self.test_rules) + h.addWidget(b) + h.addStretch(10) + self.save_button = b = QPushButton(_('&Save'), self) + b.setToolTip(_('Save this ruleset for later re-use')) + b.clicked.connect(self.save_ruleset) + h.addWidget(b) + self.export_button = b = QPushButton(_('&Load'), self) + self.load_menu = QMenu(self) + b.setMenu(self.load_menu) + b.setToolTip(_('Load a previously saved ruleset')) + b.clicked.connect(self.load_ruleset) + h.addWidget(b) + self.build_load_menu() + + def export_rules(self): + rules = self.rules_widget.rules + if not rules: + return error_dialog(self, _('No rules'), _( + 'There are no rules to export'), show=True) + path = choose_save_file(self, 'export-style-transform-rules', _('Choose file for exported rules'), initial_filename='rules.txt') + if path: + raw = export_rules(rules) + with open(path, 'wb') as f: + f.write(raw) + + def import_rules(self): + paths = choose_files(self, 'export-style-transform-rules', _('Choose file to import rules from'), select_only_single_file=True) + if paths: + with open(paths[0], 'rb') as f: + rules = import_rules(f.read()) + self.rules_widget.rules = list(rules) + list(self.rules_widget.rules) + self.changed.emit() + + def load_ruleset(self, name): + SaveLoadMixin.load_ruleset(self, name) + self.changed.emit() + + def test_rules(self): + Tester(self.rules_widget.rules, self).exec_() + + @property + def rules(self): + return self.rules_widget.rules + + @rules.setter + def rules(self, val): + try: + self.rules_widget.rules = val or [] + except Exception: + import traceback + traceback.print_exc() + self.rules_widget.rules = [] +# }}} if __name__ == '__main__': from calibre.gui2 import Application diff --git a/src/calibre/gui2/tag_mapper.py b/src/calibre/gui2/tag_mapper.py index 7be534e6d3..8449c150bb 100644 --- a/src/calibre/gui2/tag_mapper.py +++ b/src/calibre/gui2/tag_mapper.py @@ -11,7 +11,7 @@ from functools import partial from PyQt5.Qt import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QIcon, QSize, QComboBox, QLineEdit, QListWidgetItem, QStyledItemDelegate, - QStaticText, Qt, QStyle, QToolButton, QInputDialog, QMenu + QStaticText, Qt, QStyle, QToolButton, QInputDialog, QMenu, pyqtSignal ) from calibre.ebooks.metadata.tag_mapper import map_tags, compile_pat @@ -194,6 +194,7 @@ class Rules(QWidget): RuleItemClass = RuleItem RuleEditDialogClass = RuleEditDialog + changed = pyqtSignal() MSG = _('You can specify rules to filter/transform tags here. Click the "Add Rule" button' ' below to get started. The rules will be processed in order for every tag until either a' @@ -249,6 +250,7 @@ class Rules(QWidget): if d.exec_() == d.Accepted: i = self.RuleItemClass(d.edit_widget.rule, self.rule_list) self.rule_list.scrollToItem(i) + self.changed.emit() def edit_rule(self): i = self.rule_list.currentItem() @@ -259,10 +261,15 @@ class Rules(QWidget): rule = d.edit_widget.rule i.setData(DATA_ROLE, rule) i.setData(RENDER_ROLE, self.RuleItemClass.text_from_rule(rule, self.rule_list)) + self.changed.emit() def remove_rules(self): + changed = False for item in self.rule_list.selectedItems(): self.rule_list.takeItem(self.rule_list.row(item)) + changed = True + if changed: + self.changed.emit() def move_up(self): i = self.rule_list.currentItem() @@ -272,6 +279,7 @@ class Rules(QWidget): self.rule_list.takeItem(row) self.rule_list.insertItem(row - 1, i) self.rule_list.setCurrentItem(i) + self.changed.emit() def move_down(self): i = self.rule_list.currentItem() @@ -281,6 +289,7 @@ class Rules(QWidget): self.rule_list.takeItem(row) self.rule_list.insertItem(row + 1, i) self.rule_list.setCurrentItem(i) + self.changed.emit() @property def rules(self): @@ -342,41 +351,7 @@ class Tester(Dialog): ans.setWidth(ans.width() + 150) return ans -class RulesDialog(Dialog): - - DIALOG_TITLE = _('Edit tag mapper rules') - PREFS_NAME = 'edit-tag-mapper-rules' - RulesClass = Rules - TesterClass = Tester - PREFS_OBJECT = tag_maps - - def __init__(self, parent=None): - self.loaded_ruleset = None - Dialog.__init__(self, self.DIALOG_TITLE, self.PREFS_NAME, parent=parent) - - def setup_ui(self): - self.l = l = QVBoxLayout(self) - self.edit_widget = w = self.RulesClass(self) - l.addWidget(w) - l.addWidget(self.bb) - self.save_button = b = self.bb.addButton(_('&Save'), self.bb.ActionRole) - b.setToolTip(_('Save this ruleset for later re-use')) - b.clicked.connect(self.save_ruleset) - self.load_button = b = self.bb.addButton(_('&Load'), self.bb.ActionRole) - b.setToolTip(_('Load a previously saved ruleset')) - self.load_menu = QMenu(self) - b.setMenu(self.load_menu) - self.build_load_menu() - self.test_button = b = self.bb.addButton(_('&Test rules'), self.bb.ActionRole) - b.clicked.connect(self.test_rules) - - @property - def rules(self): - return self.edit_widget.rules - - @rules.setter - def rules(self, rules): - self.edit_widget.rules = rules +class SaveLoadMixin(object): def save_ruleset(self): if not self.rules: @@ -418,6 +393,42 @@ class RulesDialog(Dialog): del self.PREFS_OBJECT[name] self.build_load_menu() +class RulesDialog(Dialog, SaveLoadMixin): + + DIALOG_TITLE = _('Edit tag mapper rules') + PREFS_NAME = 'edit-tag-mapper-rules' + RulesClass = Rules + TesterClass = Tester + PREFS_OBJECT = tag_maps + + def __init__(self, parent=None): + self.loaded_ruleset = None + Dialog.__init__(self, self.DIALOG_TITLE, self.PREFS_NAME, parent=parent) + + def setup_ui(self): + self.l = l = QVBoxLayout(self) + self.edit_widget = w = self.RulesClass(self) + l.addWidget(w) + l.addWidget(self.bb) + self.save_button = b = self.bb.addButton(_('&Save'), self.bb.ActionRole) + b.setToolTip(_('Save this ruleset for later re-use')) + b.clicked.connect(self.save_ruleset) + self.load_button = b = self.bb.addButton(_('&Load'), self.bb.ActionRole) + b.setToolTip(_('Load a previously saved ruleset')) + self.load_menu = QMenu(self) + b.setMenu(self.load_menu) + self.build_load_menu() + self.test_button = b = self.bb.addButton(_('&Test rules'), self.bb.ActionRole) + b.clicked.connect(self.test_rules) + + @property + def rules(self): + return self.edit_widget.rules + + @rules.setter + def rules(self, rules): + self.edit_widget.rules = rules + def test_rules(self): self.TesterClass(self.rules, self).exec_()