%PDF- %PDF-
Direktori : /lib/calibre/calibre/gui2/preferences/ |
Current File : //lib/calibre/calibre/gui2/preferences/template_functions.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net> import copy import json import traceback from qt.core import QDialog, QDialogButtonBox from calibre.gui2 import error_dialog, gprefs, question_dialog, warning_dialog from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.preferences import AbortInitialize, ConfigWidgetBase, test_widget from calibre.gui2.preferences.template_functions_ui import Ui_Form from calibre.gui2.widgets import PythonHighlighter from calibre.utils.formatter_functions import ( compile_user_function, compile_user_template_functions, formatter_functions, function_pref_is_python, function_pref_name, load_user_template_functions ) from polyglot.builtins import iteritems, native_string_type class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): self.gui = gui self.db = gui.library_view.model().db help_text = _(''' <p>Here you can add and remove functions used in template processing. A template function is written in Python. It takes information from the book, processes it in some way, then returns a string result. Functions defined here are usable in templates in the same way that builtin functions are usable. The function must be named <b>evaluate</b>, and must have the signature shown below.</p> <p><code>evaluate(self, formatter, kwargs, mi, locals, your parameters) → returning a Unicode string</code></p> <p>The parameters of the evaluate function are: <ul> <li><b>formatter</b>: the instance of the formatter being used to evaluate the current template. You can use this to do recursive template evaluation.</li> <li><b>kwargs</b>: a dictionary of metadata. Field values are in this dictionary.</li> <li><b>mi</b>: a <i>Metadata</i> instance. Used to get field information. This parameter can be None in some cases, such as when evaluating non-book templates.</li> <li><b>locals</b>: the local variables assigned to by the current template program.</li> <li><b>your parameters</b>: you must supply one or more formal parameters. The number must match the arg count box, unless arg count is -1 (variable number or arguments), in which case the last argument must be *args. At least one argument is required, and is usually the value of the field being operated upon. Note that when writing in basic template mode, the user does not provide this first argument. Instead it is supplied by the formatter.</li> </ul></p> <p> The following example function checks the value of the field. If the field is not empty, the field's value is returned, otherwise the value EMPTY is returned. <pre> name: my_ifempty arg count: 1 doc: my_ifempty(val) -- return val if it is not empty, otherwise the string 'EMPTY' program code: def evaluate(self, formatter, kwargs, mi, locals, val): if val: return val else: return 'EMPTY'</pre> This function can be called in any of the three template program modes: <ul> <li>single-function mode: {tags:my_ifempty()}</li> <li>template program mode: {tags:'my_ifempty($)'}</li> <li>general program mode: program: my_ifempty(field('tags'))</li> </p> ''') self.textBrowser.setHtml(help_text) self.textBrowser.adjustSize() self.show_hide_help_button.clicked.connect(self.show_hide_help) self.textBrowser_showing = not gprefs.get('template_functions_prefs_tf_show_help', True) self.textBrowser_height = self.textBrowser.height() self.show_hide_help() help_text = _(''' <p> Here you can create, edit (replace), and delete stored templates used in template processing. You use a stored template in another template as if it were a template function, for example 'some_name(arg1, arg2...)'.</p> <p>Stored templates must use General Program Mode -- they must begin with the text '{0}'. You retrieve arguments passed to a stored template using the '{1}()' template function, as in '{1}(var1, var2, ...)'. The passed arguments are copied to the named variables.</p> <p>For example, this stored template checks if any items are in a list, returning '1' if any are found and '' if not.</p> <p> Template name: items_in_list<br> Template contents:<pre> program: arguments(lst='No list argument given', items=''); r = ''; for l in items: if str_in_list(lst, ',', l, '1', '') then r = '1'; break fi rof; r</pre> You call the stored template like this:<pre> program: items_in_list($#genre, 'comics, foo')</pre> See the template language tutorial for more information.</p> </p> ''') self.st_textBrowser.setHtml(help_text.format('program:', 'arguments')) self.st_textBrowser.adjustSize() self.st_show_hide_help_button.clicked.connect(self.st_show_hide_help) self.st_textBrowser_height = self.st_textBrowser.height() self.st_textBrowser_showing = not gprefs.get('template_functions_prefs_st_show_help', True) self.st_show_hide_help() def st_show_hide_help(self): gprefs['template_functions_prefs_st_show_help'] = not self.st_textBrowser_showing if self.st_textBrowser_showing: self.st_textBrowser.setMaximumHeight(self.st_show_hide_help_button.height()) self.st_textBrowser_showing = False self.st_show_hide_help_button.setText(_('Show help')) else: self.st_textBrowser.setMaximumHeight(self.st_textBrowser_height) self.st_textBrowser_showing = True self.st_show_hide_help_button.setText(_('Hide help')) def show_hide_help(self): gprefs['template_functions_prefs_tf_show_help'] = not self.textBrowser_showing if self.textBrowser_showing: self.textBrowser.setMaximumHeight(self.show_hide_help_button.height()) self.textBrowser_showing = False self.show_hide_help_button.setText(_('Show help')) else: self.textBrowser.setMaximumHeight(self.textBrowser_height) self.textBrowser_showing = True self.show_hide_help_button.setText(_('Hide help')) def initialize(self): try: self.builtin_source_dict = json.loads(P('template-functions.json', data=True, allow_user_override=False).decode('utf-8')) except: traceback.print_exc() self.builtin_source_dict = {} self.funcs = {k:v for k,v in formatter_functions().get_functions().items() if v.is_python} self.builtins = formatter_functions().get_builtins_and_aliases() self.st_funcs = {} try: for v in self.db.prefs.get('user_template_functions', []): if not function_pref_is_python(v): self.st_funcs.update({function_pref_name(v):compile_user_function(*v)}) except: if question_dialog(self, _('Template functions'), _('The template functions saved in the library are corrupt. ' "Do you want to delete them? Answering 'Yes' will delete all " "the functions."), det_msg=traceback.format_exc(), show_copy_button=True): self.db.prefs['user_template_functions'] = [] raise AbortInitialize() self.build_function_names_box() self.function_name.currentIndexChanged[native_string_type].connect(self.function_index_changed) self.function_name.editTextChanged.connect(self.function_name_edited) self.argument_count.valueChanged.connect(self.enable_replace_button) self.documentation.textChanged.connect(self.enable_replace_button) self.program.textChanged.connect(self.enable_replace_button) self.create_button.clicked.connect(self.create_button_clicked) self.delete_button.clicked.connect(self.delete_button_clicked) self.create_button.setEnabled(False) self.delete_button.setEnabled(False) self.replace_button.setEnabled(False) self.clear_button.clicked.connect(self.clear_button_clicked) self.replace_button.clicked.connect(self.replace_button_clicked) self.program.setTabStopWidth(20) self.highlighter = PythonHighlighter(self.program.document()) self.te_textbox = self.template_editor.textbox self.te_name = self.template_editor.template_name self.st_build_function_names_box() self.te_name.currentIndexChanged[native_string_type].connect(self.st_function_index_changed) self.te_name.editTextChanged.connect(self.st_template_name_edited) self.st_create_button.clicked.connect(self.st_create_button_clicked) self.st_delete_button.clicked.connect(self.st_delete_button_clicked) self.st_create_button.setEnabled(False) self.st_delete_button.setEnabled(False) self.st_replace_button.setEnabled(False) self.st_test_template_button.setEnabled(False) self.st_clear_button.clicked.connect(self.st_clear_button_clicked) self.st_test_template_button.clicked.connect(self.st_test_template) self.st_replace_button.clicked.connect(self.st_replace_button_clicked) self.st_current_program_name = '' self.st_current_program_text = '' self.st_previous_text = '' self.st_first_time = False self.st_button_layout.insertSpacing(0, 90) self.template_editor.new_doc.setFixedHeight(50) # get field metadata and selected books view = self.gui.current_view() rows = view.selectionModel().selectedRows() self.mi = [] if rows: db = view.model().db self.fm = db.field_metadata for row in rows: if row.isValid(): self.mi.append(db.new_api.get_proxy_metadata(db.data.index_to_id(row.row()))) self.template_editor.set_mi(self.mi, self.fm) # Python function tab def enable_replace_button(self): self.replace_button.setEnabled(self.delete_button.isEnabled()) def clear_button_clicked(self): self.build_function_names_box() self.program.clear() self.documentation.clear() self.argument_count.clear() self.create_button.setEnabled(False) self.delete_button.setEnabled(False) def build_function_names_box(self, scroll_to=''): self.function_name.blockSignals(True) func_names = sorted(self.funcs) self.function_name.clear() self.function_name.addItem('') self.function_name.addItems(func_names) self.function_name.setCurrentIndex(0) self.function_name.blockSignals(False) if scroll_to: idx = self.function_name.findText(scroll_to) if idx >= 0: self.function_name.setCurrentIndex(idx) if scroll_to not in self.builtins: self.delete_button.setEnabled(True) def delete_button_clicked(self): name = str(self.function_name.currentText()) if name in self.builtins: error_dialog(self.gui, _('Template functions'), _('You cannot delete a built-in function'), show=True) if name in self.funcs: del self.funcs[name] self.changed_signal.emit() self.create_button.setEnabled(True) self.delete_button.setEnabled(False) self.build_function_names_box() self.program.setReadOnly(False) else: error_dialog(self.gui, _('Template functions'), _('Function not defined'), show=True) def create_button_clicked(self, use_name=None): self.changed_signal.emit() name = use_name if use_name else str(self.function_name.currentText()) if name in self.funcs: error_dialog(self.gui, _('Template functions'), _('Name %s already used')%(name,), show=True) return if name in {function_pref_name(v) for v in self.db.prefs.get('user_template_functions', []) if not function_pref_is_python(v)}: error_dialog(self.gui, _('Template functions'), _('The name {} is already used for stored template').format(name), show=True) return if self.argument_count.value() == 0: box = warning_dialog(self.gui, _('Template functions'), _('Argument count should be -1 or greater than zero. ' 'Setting it to zero means that this function cannot ' 'be used in single function mode.'), det_msg='', show=False, show_copy_button=False) box.bb.setStandardButtons(box.bb.standardButtons() | QDialogButtonBox.StandardButton.Cancel) box.det_msg_toggle.setVisible(False) if not box.exec(): return try: prog = str(self.program.toPlainText()) cls = compile_user_function(name, str(self.documentation.toPlainText()), self.argument_count.value(), prog) self.funcs[name] = cls self.build_function_names_box(scroll_to=name) except: error_dialog(self.gui, _('Template functions'), _('Exception while compiling function'), show=True, det_msg=traceback.format_exc()) def function_name_edited(self, txt): self.documentation.setReadOnly(False) self.argument_count.setReadOnly(False) self.create_button.setEnabled(True) self.replace_button.setEnabled(False) self.program.setReadOnly(False) def function_index_changed(self, txt): txt = str(txt) self.create_button.setEnabled(False) if not txt: self.argument_count.clear() self.documentation.clear() self.documentation.setReadOnly(False) self.argument_count.setReadOnly(False) return func = self.funcs[txt] self.argument_count.setValue(func.arg_count) self.documentation.setText(func.doc) if txt in self.builtins: if hasattr(func, 'program_text') and func.program_text: self.program.setPlainText(func.program_text) elif txt in self.builtin_source_dict: self.program.setPlainText(self.builtin_source_dict[txt]) else: self.program.setPlainText(_('function source code not available')) self.documentation.setReadOnly(True) self.argument_count.setReadOnly(True) self.program.setReadOnly(True) self.delete_button.setEnabled(False) else: self.program.setPlainText(func.program_text) self.delete_button.setEnabled(True) self.program.setReadOnly(False) self.replace_button.setEnabled(False) def replace_button_clicked(self): name = str(self.function_name.currentText()) self.delete_button_clicked() self.create_button_clicked(use_name=name) def refresh_gui(self, gui): pass # Stored template tab def st_test_template(self): if self.mi: self.st_replace_button_clicked() all_funcs = copy.copy(formatter_functions().get_functions()) for n,f in self.st_funcs.items(): all_funcs[n] = f t = TemplateDialog(self.gui, self.st_previous_text, mi=self.mi, fm=self.fm, text_is_placeholder=self.st_first_time, all_functions=all_funcs) t.setWindowTitle(_('Template tester')) if t.exec() == QDialog.DialogCode.Accepted: self.st_previous_text = t.rule[1] self.st_first_time = False else: error_dialog(self.gui, _('Template functions'), _('Cannot "test" when no books are selected'), show=True) def st_clear_button_clicked(self): self.st_build_function_names_box() self.te_textbox.clear() self.template_editor.new_doc.clear() self.st_create_button.setEnabled(False) self.st_delete_button.setEnabled(False) def st_build_function_names_box(self, scroll_to=''): self.te_name.blockSignals(True) func_names = sorted(self.st_funcs) self.te_name.setMinimumContentsLength(40) self.te_name.clear() self.te_name.addItem('') self.te_name.addItems(func_names) self.te_name.setCurrentIndex(0) self.te_name.blockSignals(False) if scroll_to: idx = self.te_name.findText(scroll_to) if idx >= 0: self.te_name.setCurrentIndex(idx) def st_delete_button_clicked(self): name = str(self.te_name.currentText()) if name in self.st_funcs: del self.st_funcs[name] self.changed_signal.emit() self.st_create_button.setEnabled(True) self.st_delete_button.setEnabled(False) self.st_build_function_names_box() self.te_textbox.setReadOnly(False) self.st_current_program_name = '' else: error_dialog(self.gui, _('Stored templates'), _('Function not defined'), show=True) def st_create_button_clicked(self, use_name=None): self.changed_signal.emit() name = use_name if use_name else str(self.te_name.currentText()) for k,v in formatter_functions().get_functions().items(): if k == name and v.is_python: error_dialog(self.gui, _('Stored templates'), _('The name {} is already used for template function').format(name), show=True) try: prog = str(self.te_textbox.toPlainText()) if not prog.startswith('program:'): error_dialog(self.gui, _('Stored templates'), _('The stored template must begin with "program:"'), show=True) cls = compile_user_function(name, str(self.template_editor.new_doc.toPlainText()), 0, prog) self.st_funcs[name] = cls self.st_build_function_names_box(scroll_to=name) except: error_dialog(self.gui, _('Stored templates'), _('Exception while storing template'), show=True, det_msg=traceback.format_exc()) def st_template_name_edited(self, txt): b = txt in self.st_funcs self.st_create_button.setEnabled(not b) self.st_replace_button.setEnabled(b) self.st_delete_button.setEnabled(b) self.st_test_template_button.setEnabled(b) self.te_textbox.setReadOnly(False) def st_function_index_changed(self, txt): txt = str(txt) if self.st_current_program_name: if self.st_current_program_text != self.te_textbox.toPlainText(): box = warning_dialog(self.gui, _('Template functions'), _('Changes to the current template will be lost. OK?'), det_msg='', show=False, show_copy_button=False) box.bb.setStandardButtons(box.bb.standardButtons() | QDialogButtonBox.StandardButton.Cancel) box.det_msg_toggle.setVisible(False) if not box.exec(): self.te_name.blockSignals(True) dex = self.te_name.findText(self.st_current_program_name) self.te_name.setCurrentIndex(dex) self.te_name.blockSignals(False) return self.st_create_button.setEnabled(False) self.st_current_program_name = txt if not txt: self.te_textbox.clear() self.template_editor.new_doc.clear() return func = self.st_funcs[txt] self.st_current_program_text = func.program_text self.template_editor.new_doc.setPlainText(func.doc) self.te_textbox.setPlainText(func.program_text) self.st_template_name_edited(txt) def st_replace_button_clicked(self): name = str(self.te_name.currentText()) self.st_current_program_text = self.te_textbox.toPlainText() self.st_delete_button_clicked() self.st_create_button_clicked(use_name=name) def commit(self): pref_value = [] for name, cls in iteritems(self.funcs): if name not in self.builtins: pref_value.append(cls.to_pref()) for v in self.st_funcs.values(): pref_value.append(v.to_pref()) self.db.new_api.set_pref('user_template_functions', pref_value) funcs = compile_user_template_functions(pref_value) self.db.new_api.set_user_template_functions(funcs) self.gui.library_view.model().refresh() self.gui.library_view.model().research() load_user_template_functions(self.db.library_id, [], funcs) return False if __name__ == '__main__': from calibre import Application app = Application([]) test_widget('Advanced', 'TemplateFunctions') del app