%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/ |
| Current File : //lib/calibre/calibre/gui2/css_transform_rules.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from qt.core import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QLineEdit,
QPushButton, QSize, pyqtSignal, QMenu, QDialogButtonBox, QTextCursor
)
from calibre.ebooks.css_transform_rules import (
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 (
RuleEdit as RE, RuleEditDialog as RuleEditDialogBase, Rules as RulesBase,
RulesDialog as RulesDialogBase, RuleItem as RuleItemBase, SaveLoadMixin)
from calibre.gui2.widgets2 import Dialog
from calibre.utils.config import JSONConfig
from calibre.utils.localization import localize_user_manual_link
from polyglot.builtins import iteritems
class RuleEdit(QWidget): # {{{
MSG = _('Create the rule below, the rule can be used to transform style properties')
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.l = l = QVBoxLayout(self)
self.h = h = QHBoxLayout()
self.la = la = QLabel(self.MSG)
la.setWordWrap(True)
l.addWidget(la)
l.addLayout(h)
english_sentence = '{preamble} {property} {match_type} {query}'
sentence = _('{preamble} {property} {match_type} {query}')
if set(sentence.split()) != set(english_sentence.split()):
sentence = english_sentence
parts = sentence.split()
for clause in parts:
if clause == '{preamble}':
self.preamble = w = QLabel(_('If the &property:'))
elif clause == '{property}':
self.property = w = QLineEdit(self)
w.setToolTip(_('The name of a CSS property, for example: font-size\n'
'Do not use shorthand properties, they will not work.\n'
'For instance use margin-top, not margin.'))
elif clause == '{match_type}':
self.match_type = w = QComboBox(self)
for action, text in iteritems(MATCH_TYPE_MAP):
w.addItem(text, action)
w.currentIndexChanged.connect(self.update_state)
elif clause == '{query}':
self.query = w = QLineEdit(self)
h.addWidget(w)
if clause is not parts[-1]:
h.addWidget(QLabel('\xa0'))
self.preamble.setBuddy(self.property)
self.h2 = h = QHBoxLayout()
l.addLayout(h)
english_sentence = '{action} {action_data}'
sentence = _('{action} {action_data}')
if set(sentence.split()) != set(english_sentence.split()):
sentence = english_sentence
parts = sentence.split()
for clause in parts:
if clause == '{action}':
self.action = w = QComboBox(self)
for action, text in iteritems(ACTION_MAP):
w.addItem(text, action)
w.currentIndexChanged.connect(self.update_state)
elif clause == '{action_data}':
self.action_data = w = QLineEdit(self)
h.addWidget(w)
if clause is not parts[-1]:
h.addWidget(QLabel('\xa0'))
self.regex_help = la = QLabel('<p>' + RE.REGEXP_HELP_TEXT % localize_user_manual_link(
'https://manual.calibre-ebook.com/regexp.html'))
la.setOpenExternalLinks(True)
la.setWordWrap(True)
l.addWidget(la)
l.addStretch(10)
self.update_state()
def sizeHint(self):
a = QWidget.sizeHint(self)
a.setHeight(a.height() + 75)
a.setWidth(a.width() + 100)
return a
def update_state(self):
r = self.rule
self.action_data.setVisible(r['action'] != 'remove')
tt = _('The CSS property value')
mt = r['match_type']
self.query.setVisible(mt != '*')
if 'matches' in mt:
tt = _('A regular expression')
elif mt in '< > <= >='.split():
tt = _('Either a CSS length, such as 10pt or a unit less number. If a unit less'
' number is used it will be compared with the CSS value using whatever unit'
' the value has. Note that comparison automatically converts units, except'
' for relative units like percentage or em, for which comparison fails'
' if the units are different.')
self.query.setToolTip(tt)
tt = ''
ac = r['action']
if ac == 'append':
tt = _('CSS properties to add to the rule that contains the matching style. You'
' can specify more than one property, separated by semi-colons, for example:'
' color:red; font-weight: bold')
elif ac in '+=*/':
tt = _('A number')
self.action_data.setToolTip(tt)
self.regex_help.setVisible('matches' in mt)
@property
def rule(self):
return {
'property':self.property.text().strip().lower(),
'match_type': self.match_type.currentData(),
'query': self.query.text().strip(),
'action': self.action.currentData(),
'action_data': self.action_data.text().strip(),
}
@rule.setter
def rule(self, rule):
def sc(name):
c = getattr(self, name)
idx = c.findData(str(rule.get(name, '')))
if idx < 0:
idx = 0
c.setCurrentIndex(idx)
sc('action'), sc('match_type')
self.property.setText(str(rule.get('property', '')).strip())
self.query.setText(str(rule.get('query', '')).strip())
self.action_data.setText(str(rule.get('action_data', '')).strip())
self.update_state()
def validate(self):
rule = self.rule
title, msg = validate_rule(rule)
if msg is not None and title is not None:
error_dialog(self, title, msg, show=True)
return False
return True
# }}}
class RuleEditDialog(RuleEditDialogBase): # {{{
PREFS_NAME = 'edit-css-transform-rule'
DIALOG_TITLE = _('Edit rule')
RuleEditClass = RuleEdit
# }}}
class RuleItem(RuleItemBase): # {{{
@staticmethod
def text_from_rule(rule, parent):
try:
query = elided_text(rule['query'], font=parent.font(), width=200, pos='right')
text = _(
'If the property <i>{property}</i> <b>{match_type}</b> <b>{query}</b><br>{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 += ' <code>%s</code>' % ad
except Exception:
import traceback
traceback.print_exc()
text = _('This rule is invalid, please remove it')
return text
# }}}
class Rules(RulesBase): # {{{
RuleItemClass = RuleItem
RuleEditDialogClass = RuleEditDialog
MSG = _('You can specify rules to transform styles here. Click the "Add rule" button'
' below to get started.')
# }}}
class Tester(Dialog): # {{{
DIALOG_TITLE = _('Test style transform rules')
PREFS_NAME = 'test-style-transform-rules'
LABEL = _('Enter a CSS stylesheet below and click the "Test" button')
SYNTAX = 'css'
RESULTS = '/* %s */\n\n' % _('Resulting stylesheet')
def __init__(self, rules, parent=None):
self.rules = self.compile_rules(rules)
Dialog.__init__(self, self.DIALOG_TITLE, self.PREFS_NAME, parent=parent)
def compile_rules(self, rules):
return compile_rules(rules)
def setup_ui(self):
from calibre.gui2.tweak_book.editor.text import TextEdit
self.l = l = QVBoxLayout(self)
self.bb.setStandardButtons(QDialogButtonBox.StandardButton.Close)
self.la = la = QLabel(self.LABEL)
l.addWidget(la)
self.css = t = TextEdit(self)
t.load_text('', self.SYNTAX)
la.setBuddy(t)
c = t.textCursor()
c.movePosition(QTextCursor.MoveOperation.End)
t.setTextCursor(c)
self.h = h = QHBoxLayout()
l.addLayout(h)
h.addWidget(t)
self.test_button = b = QPushButton(_('&Test'), self)
b.clicked.connect(self.do_test)
h.addWidget(b)
self.result = la = TextEdit(self)
la.setReadOnly(True)
l.addWidget(la)
l.addWidget(self.bb)
@property
def value(self):
return self.css.toPlainText()
def do_test(self):
decl = safe_parser().parseString(self.value)
transform_sheet(self.rules, decl)
css = decl.cssText
if isinstance(css, bytes):
css = css.decode('utf-8')
self.set_result(css)
def set_result(self, css):
self.result.load_text(self.RESULTS + css, self.SYNTAX)
def sizeHint(self):
return QSize(800, 600)
# }}}
class RulesDialog(RulesDialogBase): # {{{
DIALOG_TITLE = _('Edit style transform rules')
PREFS_NAME = 'edit-style-transform-rules'
PREFS_OBJECT_NAME = 'style-transform-rules'
RulesClass = Rules
TesterClass = Tester
def __init__(self, *args, **kw):
# This has to be loaded on instantiation as it can be shared by
# multiple processes
self.PREFS_OBJECT = JSONConfig(self.PREFS_OBJECT_NAME)
RulesDialogBase.__init__(self, *args, **kw)
# }}}
class RulesWidget(QWidget, SaveLoadMixin): # {{{
changed = pyqtSignal()
PREFS_NAME = 'style-transform-rules'
INITIAL_FILE_NAME = 'css-rules.txt'
DIR_SAVE_NAME = 'export-style-transform-rules'
export_func = export_rules
import_func = import_rules
TesterClass = Tester
RulesClass = Rules
def __init__(self, parent=None):
self.loaded_ruleset = None
QWidget.__init__(self, parent)
self.PREFS_OBJECT = JSONConfig(self.PREFS_NAME)
l = QVBoxLayout(self)
self.rules_widget = w = self.RulesClass(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.load_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, self.DIR_SAVE_NAME, _('Choose file for exported rules'), initial_filename=self.INITIAL_FILE_NAME)
if path:
f = self.__class__.export_func
raw = f(rules)
with open(path, 'wb') as f:
f.write(raw)
def import_rules(self):
paths = choose_files(self, self.DIR_SAVE_NAME, _('Choose file to import rules from'), select_only_single_file=True)
if paths:
func = self.__class__.import_func
with open(paths[0], 'rb') as f:
rules = func(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):
self.TesterClass(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
app = Application([])
d = RulesDialog()
d.rules = [
{'property':'color', 'match_type':'*', 'query':'', 'action':'change', 'action_data':'green'},
]
d.exec()
from pprint import pprint
pprint(d.rules)
del d, app