mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Merge branch 'StoredTemplates'
This commit is contained in:
commit
09b56dd006
@ -935,6 +935,12 @@ class ActionTemplateTester(InterfaceActionBase):
|
|||||||
description = _('Show an editor for testing templates')
|
description = _('Show an editor for testing templates')
|
||||||
|
|
||||||
|
|
||||||
|
class ActionStoredTemplates(InterfaceActionBase):
|
||||||
|
name = 'Stored Templates'
|
||||||
|
actual_plugin = 'calibre.gui2.actions.show_stored_templates:ShowStoredTemplatesAction'
|
||||||
|
description = _('Show a dialog for creating and managing stored templates')
|
||||||
|
|
||||||
|
|
||||||
class ActionSaveToDisk(InterfaceActionBase):
|
class ActionSaveToDisk(InterfaceActionBase):
|
||||||
name = 'Save To Disk'
|
name = 'Save To Disk'
|
||||||
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
|
actual_plugin = 'calibre.gui2.actions.save_to_disk:SaveToDiskAction'
|
||||||
@ -1099,7 +1105,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
|||||||
ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore,
|
ActionCopyToLibrary, ActionTweakEpub, ActionUnpackBook, ActionNextMatch, ActionStore,
|
||||||
ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy,
|
ActionPluginUpdater, ActionPickRandom, ActionEditToC, ActionSortBy,
|
||||||
ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper,
|
ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper,
|
||||||
ActionVirtualLibrary, ActionBrowseAnnotations]
|
ActionVirtualLibrary, ActionBrowseAnnotations, ActionStoredTemplates]
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -1272,6 +1278,18 @@ class TemplateFunctions(PreferencesPlugin):
|
|||||||
description = _('Create your own template functions')
|
description = _('Create your own template functions')
|
||||||
|
|
||||||
|
|
||||||
|
class StoredTemplates(PreferencesPlugin):
|
||||||
|
name = 'StoredTemplates'
|
||||||
|
icon = I('template_funcs.png')
|
||||||
|
gui_name = _('Stored templates')
|
||||||
|
category = 'Advanced'
|
||||||
|
gui_category = _('Advanced')
|
||||||
|
category_order = 5
|
||||||
|
name_order = 6
|
||||||
|
config_widget = 'calibre.gui2.preferences.stored_templates'
|
||||||
|
description = _('Create stored calibre templates')
|
||||||
|
|
||||||
|
|
||||||
class Email(PreferencesPlugin):
|
class Email(PreferencesPlugin):
|
||||||
name = 'Email'
|
name = 'Email'
|
||||||
icon = I('mail.png')
|
icon = I('mail.png')
|
||||||
@ -1376,7 +1394,7 @@ class Misc(PreferencesPlugin):
|
|||||||
plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
|
plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
|
||||||
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
|
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
|
||||||
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions,
|
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions,
|
||||||
MetadataSources, Keyboard, IgnoredDevices]
|
StoredTemplates, MetadataSources, Keyboard, IgnoredDevices]
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
30
src/calibre/gui2/actions/show_stored_templates.py
Normal file
30
src/calibre/gui2/actions/show_stored_templates.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.gui2.preferences.main import Preferences
|
||||||
|
|
||||||
|
|
||||||
|
class ShowStoredTemplatesAction(InterfaceAction):
|
||||||
|
|
||||||
|
name = 'Stored Template'
|
||||||
|
action_spec = (_('Stored Templates'), 'debug.png', None, ())
|
||||||
|
dont_add_to = frozenset(('context-menu-device',))
|
||||||
|
action_type = 'current'
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.previous_text = _('Manage stored templates')
|
||||||
|
self.first_time = True
|
||||||
|
self.qaction.triggered.connect(self.show_template_editor)
|
||||||
|
|
||||||
|
def show_template_editor(self, *args):
|
||||||
|
d = Preferences(self.gui, initial_plugin=('Advanced', 'StoredTemplates'),
|
||||||
|
close_after_initial=True)
|
||||||
|
d.exec_()
|
||||||
|
|
@ -429,12 +429,17 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
|||||||
name = unicode_type(toWhat)
|
name = unicode_type(toWhat)
|
||||||
self.source_code.clear()
|
self.source_code.clear()
|
||||||
self.documentation.clear()
|
self.documentation.clear()
|
||||||
|
self.func_type.clear()
|
||||||
if name in self.funcs:
|
if name in self.funcs:
|
||||||
self.documentation.setPlainText(self.funcs[name].doc)
|
self.documentation.setPlainText(self.funcs[name].doc)
|
||||||
if name in self.builtins and name in self.builtin_source_dict:
|
if name in self.builtins and name in self.builtin_source_dict:
|
||||||
self.source_code.setPlainText(self.builtin_source_dict[name])
|
self.source_code.setPlainText(self.builtin_source_dict[name])
|
||||||
else:
|
else:
|
||||||
self.source_code.setPlainText(self.funcs[name].program_text)
|
self.source_code.setPlainText(self.funcs[name].program_text)
|
||||||
|
if self.funcs[name].is_python:
|
||||||
|
self.func_type.setText(_('Template function in python'))
|
||||||
|
else:
|
||||||
|
self.func_type.setText(_('Stored template'))
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
txt = unicode_type(self.textbox.toPlainText()).rstrip()
|
txt = unicode_type(self.textbox.toPlainText()).rstrip()
|
||||||
|
@ -224,6 +224,26 @@
|
|||||||
<widget class="QComboBox" name="function"/>
|
<widget class="QComboBox" name="function"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0">
|
<item row="9" column="0">
|
||||||
|
<widget class="QLabel" name="label_22">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Function type:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>func_type</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="9" column="1">
|
||||||
|
<widget class="QLineEdit" name="func_type">
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="10" column="0">
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_2">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Documentation:</string>
|
<string>&Documentation:</string>
|
||||||
@ -236,10 +256,10 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="10" column="0">
|
<item row="11" column="0">
|
||||||
<widget class="QLabel" name="label_3">
|
<widget class="QLabel" name="label_3">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Python &code:</string>
|
<string>&Code:</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||||
@ -249,7 +269,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="1">
|
<item row="10" column="1">
|
||||||
<widget class="QPlainTextEdit" name="documentation">
|
<widget class="QPlainTextEdit" name="documentation">
|
||||||
<property name="maximumSize">
|
<property name="maximumSize">
|
||||||
<size>
|
<size>
|
||||||
@ -259,17 +279,17 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="10" column="1">
|
<item row="11" column="1">
|
||||||
<widget class="QPlainTextEdit" name="source_code"/>
|
<widget class="QPlainTextEdit" name="source_code"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="11" column="1">
|
<item row="12" column="1">
|
||||||
<widget class="QLabel" name="template_tutorial">
|
<widget class="QLabel" name="template_tutorial">
|
||||||
<property name="openExternalLinks">
|
<property name="openExternalLinks">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="12" column="1">
|
<item row="13" column="1">
|
||||||
<widget class="QLabel" name="template_func_reference">
|
<widget class="QLabel" name="template_func_reference">
|
||||||
<property name="openExternalLinks">
|
<property name="openExternalLinks">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
212
src/calibre/gui2/preferences/stored_templates.py
Normal file
212
src/calibre/gui2/preferences/stored_templates.py
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from PyQt5.Qt import (Qt, QGridLayout, QLabel, QSpacerItem, QSizePolicy,
|
||||||
|
QComboBox, QTextEdit, QHBoxLayout, QPushButton)
|
||||||
|
|
||||||
|
from calibre.gui2 import error_dialog
|
||||||
|
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
|
||||||
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||||
|
from calibre.utils.formatter_functions import (formatter_functions,
|
||||||
|
compile_user_function, compile_user_template_functions,
|
||||||
|
load_user_template_functions, function_pref_is_python,
|
||||||
|
function_pref_name)
|
||||||
|
from polyglot.builtins import native_string_type, unicode_type
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigWidget(ConfigWidgetBase):
|
||||||
|
|
||||||
|
def genesis(self, gui):
|
||||||
|
self.gui = gui
|
||||||
|
self.db = gui.library_view.model().db
|
||||||
|
|
||||||
|
help_text = '<p>' + _('''
|
||||||
|
Here you can add and remove stored templates used in template processing.
|
||||||
|
You use a stored template in another template with the 'call' template
|
||||||
|
function, as in 'call(somename, arguments...). Stored templates must use
|
||||||
|
General Program Mode -- they must begin with the text 'program:'.
|
||||||
|
In the stored template you get the arguments using the 'arguments()'
|
||||||
|
template function, as in arguments(var1, var2, ...). The calling arguments
|
||||||
|
are copied to the named variables.
|
||||||
|
''') + '</p>'
|
||||||
|
l = QGridLayout(self)
|
||||||
|
w = QLabel(help_text)
|
||||||
|
w.setWordWrap(True)
|
||||||
|
l.addWidget(w, 0, 0, 1, 2)
|
||||||
|
|
||||||
|
lab = QLabel(_('&Template'))
|
||||||
|
l.addWidget(lab, 1, 0)
|
||||||
|
lb = QHBoxLayout()
|
||||||
|
self.program = w = TemplateLineEditor(self)
|
||||||
|
w.setPlaceholderText(_('The GPM template.'))
|
||||||
|
lab.setBuddy(w)
|
||||||
|
lb.addWidget(w, stretch=1)
|
||||||
|
self.editor_button = b = QPushButton(_('&Open Editor'))
|
||||||
|
b.clicked.connect(w.open_editor)
|
||||||
|
lb.addWidget(b)
|
||||||
|
l.addLayout(lb, 1, 1)
|
||||||
|
|
||||||
|
lab = QLabel(_('&Name'))
|
||||||
|
l.addWidget(lab, 2, 0)
|
||||||
|
self.function_name = w = QComboBox(self)
|
||||||
|
w.setEditable(True)
|
||||||
|
lab.setBuddy(w)
|
||||||
|
w.setToolTip(_('The name of the function, used in a call statement'))
|
||||||
|
|
||||||
|
l.addWidget(w, 2, 1)
|
||||||
|
|
||||||
|
lab = QLabel(_('&Documentation'))
|
||||||
|
l.addWidget(lab, 3, 0, Qt.AlignTop)
|
||||||
|
self.documentation = w = QTextEdit(self)
|
||||||
|
w.setPlaceholderText(_('A description of the template. Whatever you wish ...'))
|
||||||
|
lab.setBuddy(w)
|
||||||
|
l.addWidget(w, 3, 1)
|
||||||
|
|
||||||
|
lb = QHBoxLayout()
|
||||||
|
lb.addStretch(1)
|
||||||
|
self.clear_button = w = QPushButton(_('C&lear'))
|
||||||
|
lb.addWidget(w)
|
||||||
|
self.delete_button = w = QPushButton(_('&Delete'))
|
||||||
|
lb.addWidget(w)
|
||||||
|
self.replace_button = w = QPushButton(_('&Replace'))
|
||||||
|
lb.addWidget(w)
|
||||||
|
self.create_button = w = QPushButton(_('&Create'))
|
||||||
|
lb.addWidget(w)
|
||||||
|
lb.addStretch(1)
|
||||||
|
l.addLayout(lb, 9, 1)
|
||||||
|
|
||||||
|
l.addItem(QSpacerItem(10, 10, vPolicy=QSizePolicy.Expanding), 10, 0, -1, -1)
|
||||||
|
self.setLayout(l)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.funcs = {}
|
||||||
|
for v in self.db.prefs.get('user_template_functions', []):
|
||||||
|
if not function_pref_is_python(v):
|
||||||
|
self.funcs.update({function_pref_name(v):compile_user_function(*v)})
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
|
||||||
|
def delete_button_clicked(self):
|
||||||
|
name = unicode_type(self.function_name.currentText())
|
||||||
|
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, _('Stored templates'),
|
||||||
|
_('Function not defined'), show=True)
|
||||||
|
|
||||||
|
def create_button_clicked(self, use_name=None):
|
||||||
|
self.changed_signal.emit()
|
||||||
|
name = use_name if use_name else unicode_type(self.function_name.currentText())
|
||||||
|
for k,v in formatter_functions().get_functions().items():
|
||||||
|
if k == name and v.is_python:
|
||||||
|
error_dialog(self.gui, _('Stored templates'),
|
||||||
|
_('Name %s is already used for template function')%(name,), show=True)
|
||||||
|
try:
|
||||||
|
prog = unicode_type(self.program.text())
|
||||||
|
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, unicode_type(self.documentation.toPlainText()),
|
||||||
|
0, prog)
|
||||||
|
self.funcs[name] = cls
|
||||||
|
self.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 function_name_edited(self, txt):
|
||||||
|
self.documentation.setReadOnly(False)
|
||||||
|
self.create_button.setEnabled(True)
|
||||||
|
self.replace_button.setEnabled(False)
|
||||||
|
self.program.setReadOnly(False)
|
||||||
|
|
||||||
|
def function_index_changed(self, txt):
|
||||||
|
txt = unicode_type(txt)
|
||||||
|
self.create_button.setEnabled(False)
|
||||||
|
if not txt:
|
||||||
|
self.program.clear()
|
||||||
|
self.documentation.clear()
|
||||||
|
self.documentation.setReadOnly(False)
|
||||||
|
return
|
||||||
|
func = self.funcs[txt]
|
||||||
|
self.documentation.setText(func.doc)
|
||||||
|
self.program.setText(func.program_text)
|
||||||
|
self.delete_button.setEnabled(True)
|
||||||
|
self.program.setReadOnly(False)
|
||||||
|
self.replace_button.setEnabled(False)
|
||||||
|
|
||||||
|
def replace_button_clicked(self):
|
||||||
|
name = unicode_type(self.function_name.currentText())
|
||||||
|
self.delete_button_clicked()
|
||||||
|
self.create_button_clicked(use_name=name)
|
||||||
|
|
||||||
|
def refresh_gui(self, gui):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
# formatter_functions().reset_to_builtins()
|
||||||
|
pref_value = [v for v in self.db.prefs.get('user_template_functions', [])
|
||||||
|
if function_pref_is_python(v)]
|
||||||
|
for v in self.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()
|
||||||
|
load_user_template_functions(self.db.library_id, [], funcs)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt5.Qt import QApplication
|
||||||
|
app = QApplication([])
|
||||||
|
test_widget('Advanced', 'StoredTemplates')
|
@ -16,7 +16,8 @@ from calibre.gui2.preferences.template_functions_ui import Ui_Form
|
|||||||
from calibre.gui2.widgets import PythonHighlighter
|
from calibre.gui2.widgets import PythonHighlighter
|
||||||
from calibre.utils.formatter_functions import (formatter_functions,
|
from calibre.utils.formatter_functions import (formatter_functions,
|
||||||
compile_user_function, compile_user_template_functions,
|
compile_user_function, compile_user_template_functions,
|
||||||
load_user_template_functions)
|
load_user_template_functions, function_pref_is_python,
|
||||||
|
function_pref_name)
|
||||||
from polyglot.builtins import iteritems, native_string_type, unicode_type
|
from polyglot.builtins import iteritems, native_string_type, unicode_type
|
||||||
|
|
||||||
|
|
||||||
@ -86,7 +87,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self.builtin_source_dict = {}
|
self.builtin_source_dict = {}
|
||||||
|
|
||||||
self.funcs = formatter_functions().get_functions()
|
self.funcs = dict((k,v) for k,v in formatter_functions().get_functions().items()
|
||||||
|
if v.is_python)
|
||||||
|
|
||||||
self.builtins = formatter_functions().get_builtins_and_aliases()
|
self.builtins = formatter_functions().get_builtins_and_aliases()
|
||||||
|
|
||||||
self.build_function_names_box()
|
self.build_function_names_box()
|
||||||
@ -116,16 +119,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.create_button.setEnabled(False)
|
self.create_button.setEnabled(False)
|
||||||
self.delete_button.setEnabled(False)
|
self.delete_button.setEnabled(False)
|
||||||
|
|
||||||
def build_function_names_box(self, scroll_to='', set_to=''):
|
def build_function_names_box(self, scroll_to=''):
|
||||||
self.function_name.blockSignals(True)
|
self.function_name.blockSignals(True)
|
||||||
func_names = sorted(self.funcs)
|
func_names = sorted(self.funcs)
|
||||||
self.function_name.clear()
|
self.function_name.clear()
|
||||||
self.function_name.addItem('')
|
self.function_name.addItem('')
|
||||||
self.function_name.addItems(func_names)
|
self.function_name.addItems(func_names)
|
||||||
self.function_name.setCurrentIndex(0)
|
self.function_name.setCurrentIndex(0)
|
||||||
if set_to:
|
|
||||||
self.function_name.setEditText(set_to)
|
|
||||||
self.create_button.setEnabled(True)
|
|
||||||
self.function_name.blockSignals(False)
|
self.function_name.blockSignals(False)
|
||||||
if scroll_to:
|
if scroll_to:
|
||||||
idx = self.function_name.findText(scroll_to)
|
idx = self.function_name.findText(scroll_to)
|
||||||
@ -140,23 +140,30 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
error_dialog(self.gui, _('Template functions'),
|
error_dialog(self.gui, _('Template functions'),
|
||||||
_('You cannot delete a built-in function'), show=True)
|
_('You cannot delete a built-in function'), show=True)
|
||||||
if name in self.funcs:
|
if name in self.funcs:
|
||||||
|
print('delete')
|
||||||
del self.funcs[name]
|
del self.funcs[name]
|
||||||
self.changed_signal.emit()
|
self.changed_signal.emit()
|
||||||
self.create_button.setEnabled(True)
|
self.create_button.setEnabled(True)
|
||||||
self.delete_button.setEnabled(False)
|
self.delete_button.setEnabled(False)
|
||||||
self.build_function_names_box(set_to=name)
|
self.build_function_names_box()
|
||||||
self.program.setReadOnly(False)
|
self.program.setReadOnly(False)
|
||||||
else:
|
else:
|
||||||
error_dialog(self.gui, _('Template functions'),
|
error_dialog(self.gui, _('Template functions'),
|
||||||
_('Function not defined'), show=True)
|
_('Function not defined'), show=True)
|
||||||
|
|
||||||
def create_button_clicked(self):
|
def create_button_clicked(self, use_name=None):
|
||||||
self.changed_signal.emit()
|
self.changed_signal.emit()
|
||||||
name = unicode_type(self.function_name.currentText())
|
name = use_name if use_name else unicode_type(self.function_name.currentText())
|
||||||
if name in self.funcs:
|
if name in self.funcs:
|
||||||
error_dialog(self.gui, _('Template functions'),
|
error_dialog(self.gui, _('Template functions'),
|
||||||
_('Name %s already used')%(name,), show=True)
|
_('Name %s already used')%(name,), show=True)
|
||||||
return
|
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'),
|
||||||
|
_('Name %s is already used for stored template')%(name,), show=True)
|
||||||
|
return
|
||||||
if self.argument_count.value() == 0:
|
if self.argument_count.value() == 0:
|
||||||
box = warning_dialog(self.gui, _('Template functions'),
|
box = warning_dialog(self.gui, _('Template functions'),
|
||||||
_('Argument count should be -1 or greater than zero. '
|
_('Argument count should be -1 or greater than zero. '
|
||||||
@ -215,18 +222,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.replace_button.setEnabled(False)
|
self.replace_button.setEnabled(False)
|
||||||
|
|
||||||
def replace_button_clicked(self):
|
def replace_button_clicked(self):
|
||||||
|
name = unicode_type(self.function_name.currentText())
|
||||||
self.delete_button_clicked()
|
self.delete_button_clicked()
|
||||||
self.create_button_clicked()
|
self.create_button_clicked(use_name=name)
|
||||||
|
|
||||||
def refresh_gui(self, gui):
|
def refresh_gui(self, gui):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
# formatter_functions().reset_to_builtins()
|
pref_value = [v for v in self.db.prefs.get('user_template_functions', [])
|
||||||
pref_value = []
|
if not function_pref_is_python(v)]
|
||||||
for name, cls in iteritems(self.funcs):
|
for name, cls in iteritems(self.funcs):
|
||||||
if name not in self.builtins:
|
if name not in self.builtins:
|
||||||
pref_value.append((cls.name, cls.doc, cls.arg_count, cls.program_text))
|
pref_value.append(cls.to_pref())
|
||||||
self.db.new_api.set_pref('user_template_functions', pref_value)
|
self.db.new_api.set_pref('user_template_functions', pref_value)
|
||||||
funcs = compile_user_template_functions(pref_value)
|
funcs = compile_user_template_functions(pref_value)
|
||||||
self.db.new_api.set_user_template_functions(funcs)
|
self.db.new_api.set_user_template_functions(funcs)
|
||||||
|
@ -28,6 +28,8 @@ class Node(object):
|
|||||||
NODE_CONSTANT = 7
|
NODE_CONSTANT = 7
|
||||||
NODE_FIELD = 8
|
NODE_FIELD = 8
|
||||||
NODE_RAW_FIELD = 9
|
NODE_RAW_FIELD = 9
|
||||||
|
NODE_CALL = 10
|
||||||
|
NODE_ARGUMENTS = 11
|
||||||
|
|
||||||
|
|
||||||
class IfNode(Node):
|
class IfNode(Node):
|
||||||
@ -55,6 +57,21 @@ class FunctionNode(Node):
|
|||||||
self.expression_list = expression_list
|
self.expression_list = expression_list
|
||||||
|
|
||||||
|
|
||||||
|
class CallNode(Node):
|
||||||
|
def __init__(self, function, expression_list):
|
||||||
|
Node.__init__(self)
|
||||||
|
self.node_type = self.NODE_CALL
|
||||||
|
self.function = function
|
||||||
|
self.expression_list = expression_list
|
||||||
|
|
||||||
|
|
||||||
|
class ArgumentsNode(Node):
|
||||||
|
def __init__(self, expression_list):
|
||||||
|
Node.__init__(self)
|
||||||
|
self.node_type = self.NODE_ARGUMENTS
|
||||||
|
self.expression_list = expression_list
|
||||||
|
|
||||||
|
|
||||||
class StringInfixNode(Node):
|
class StringInfixNode(Node):
|
||||||
def __init__(self, operator, left, right):
|
def __init__(self, operator, left, right):
|
||||||
Node.__init__(self)
|
Node.__init__(self)
|
||||||
@ -188,6 +205,13 @@ class _Parser(object):
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def token_is_call(self):
|
||||||
|
try:
|
||||||
|
token = self.prog[self.lex_pos]
|
||||||
|
return token[1] == 'call' and token[0] == self.LEX_KEYWORD
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def token_is_if(self):
|
def token_is_if(self):
|
||||||
try:
|
try:
|
||||||
token = self.prog[self.lex_pos]
|
token = self.prog[self.lex_pos]
|
||||||
@ -228,9 +252,11 @@ class _Parser(object):
|
|||||||
except:
|
except:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def program(self, funcs, prog):
|
def program(self, parent, funcs, prog):
|
||||||
self.lex_pos = 0
|
self.lex_pos = 0
|
||||||
|
self.parent = parent
|
||||||
self.funcs = funcs
|
self.funcs = funcs
|
||||||
|
self.func_names = frozenset(set(self.funcs.keys()) | {'arguments',})
|
||||||
self.prog = prog[0]
|
self.prog = prog[0]
|
||||||
self.prog_len = len(self.prog)
|
self.prog_len = len(self.prog)
|
||||||
if prog[1] != '':
|
if prog[1] != '':
|
||||||
@ -276,9 +302,37 @@ class _Parser(object):
|
|||||||
return NumericInfixNode(operator, left, self.expr())
|
return NumericInfixNode(operator, left, self.expr())
|
||||||
return left
|
return left
|
||||||
|
|
||||||
|
def call_expression(self):
|
||||||
|
self.consume()
|
||||||
|
if not self.token_is_id():
|
||||||
|
self.error(_('"call" requires a stored template name'))
|
||||||
|
name = self.token()
|
||||||
|
if name not in self.func_names or self.funcs[name].is_python:
|
||||||
|
self.error(_('{} is not a stored template').format(name))
|
||||||
|
text = self.funcs[name].program_text
|
||||||
|
if not text.startswith('program:'):
|
||||||
|
self.error((_('A stored template must begin with program:')))
|
||||||
|
text = text[len('program:'):]
|
||||||
|
if not self.token_op_is_lparen():
|
||||||
|
self.error(_('"call" requires arguments surrounded by "(" ")"'))
|
||||||
|
self.consume()
|
||||||
|
arguments = list()
|
||||||
|
while not self.token_op_is_rparen():
|
||||||
|
arguments.append(self.infix_expr())
|
||||||
|
if not self.token_op_is_comma():
|
||||||
|
break
|
||||||
|
self.consume()
|
||||||
|
if self.token() != ')':
|
||||||
|
self.error(_('Missing closing parenthesis'))
|
||||||
|
subprog = _Parser().program(self, self.funcs,
|
||||||
|
self.parent.lex_scanner.scan(text))
|
||||||
|
return CallNode(subprog, arguments)
|
||||||
|
|
||||||
def expr(self):
|
def expr(self):
|
||||||
if self.token_is_if():
|
if self.token_is_if():
|
||||||
return self.if_expression()
|
return self.if_expression()
|
||||||
|
if self.token_is_call():
|
||||||
|
return self.call_expression()
|
||||||
if self.token_is_id():
|
if self.token_is_id():
|
||||||
# We have an identifier. Determine if it is a function
|
# We have an identifier. Determine if it is a function
|
||||||
id_ = self.token()
|
id_ = self.token()
|
||||||
@ -293,7 +347,7 @@ class _Parser(object):
|
|||||||
# Check if it is a known one. We do this here so error reporting is
|
# Check if it is a known one. We do this here so error reporting is
|
||||||
# better, as it can identify the tokens near the problem.
|
# better, as it can identify the tokens near the problem.
|
||||||
id_ = id_.strip()
|
id_ = id_.strip()
|
||||||
if id_ not in self.funcs:
|
if id_ not in self.func_names:
|
||||||
self.error(_('Unknown function {0}').format(id_))
|
self.error(_('Unknown function {0}').format(id_))
|
||||||
# Eat the paren
|
# Eat the paren
|
||||||
self.consume()
|
self.consume()
|
||||||
@ -314,9 +368,19 @@ class _Parser(object):
|
|||||||
return IfNode(arguments[0], (arguments[1],), (arguments[2],))
|
return IfNode(arguments[0], (arguments[1],), (arguments[2],))
|
||||||
if (id_ == 'assign' and len(arguments) == 2 and arguments[0].node_type == Node.NODE_RVALUE):
|
if (id_ == 'assign' and len(arguments) == 2 and arguments[0].node_type == Node.NODE_RVALUE):
|
||||||
return AssignNode(arguments[0].name, arguments[1])
|
return AssignNode(arguments[0].name, arguments[1])
|
||||||
|
if id_ == 'arguments':
|
||||||
|
new_args = []
|
||||||
|
for arg in arguments:
|
||||||
|
if arg.node_type not in (Node.NODE_ASSIGN, Node.NODE_RVALUE):
|
||||||
|
self.error(_("Parameters to 'arguments' must be "
|
||||||
|
"variables or assignments"))
|
||||||
|
if arg.node_type == Node.NODE_RVALUE:
|
||||||
|
arg = AssignNode(arg.name, ConstantNode(''))
|
||||||
|
new_args.append(arg)
|
||||||
|
return ArgumentsNode(new_args)
|
||||||
cls = self.funcs[id_]
|
cls = self.funcs[id_]
|
||||||
if cls.arg_count != -1 and len(arguments) != cls.arg_count:
|
if cls.arg_count != -1 and len(arguments) != cls.arg_count:
|
||||||
self.error(_('Incorrect number of expression_list for function {0}').format(id_))
|
self.error(_('Incorrect number of arguments for function {0}').format(id_))
|
||||||
return FunctionNode(id_, arguments)
|
return FunctionNode(id_, arguments)
|
||||||
elif self.token_is_constant():
|
elif self.token_is_constant():
|
||||||
# String or number
|
# String or number
|
||||||
@ -408,6 +472,24 @@ class _Interpreter(object):
|
|||||||
return cls.eval_(self.parent, self.parent_kwargs,
|
return cls.eval_(self.parent, self.parent_kwargs,
|
||||||
self.parent_book, self.locals, *args)
|
self.parent_book, self.locals, *args)
|
||||||
|
|
||||||
|
def do_node_call(self, prog):
|
||||||
|
args = list()
|
||||||
|
for arg in prog.expression_list:
|
||||||
|
# evaluate the expression (recursive call)
|
||||||
|
args.append(self.expr(arg))
|
||||||
|
saved_locals = self.locals
|
||||||
|
self.locals = {}
|
||||||
|
for dex, v in enumerate(args):
|
||||||
|
self.locals['*arg_'+ str(dex)] = v
|
||||||
|
val = self.expression_list(prog.function)
|
||||||
|
self.locals = saved_locals
|
||||||
|
return val
|
||||||
|
|
||||||
|
def do_node_arguments(self, prog):
|
||||||
|
for dex,arg in enumerate(prog.expression_list):
|
||||||
|
self.locals[arg.left] = self.locals.get('*arg_'+ str(dex), self.expr(arg.right))
|
||||||
|
return ''
|
||||||
|
|
||||||
def do_node_constant(self, prog):
|
def do_node_constant(self, prog):
|
||||||
return prog.value
|
return prog.value
|
||||||
|
|
||||||
@ -454,6 +536,8 @@ class _Interpreter(object):
|
|||||||
Node.NODE_RAW_FIELD: do_node_raw_field,
|
Node.NODE_RAW_FIELD: do_node_raw_field,
|
||||||
Node.NODE_STRING_INFIX: do_node_string_infix,
|
Node.NODE_STRING_INFIX: do_node_string_infix,
|
||||||
Node.NODE_NUMERIC_INFIX: do_node_numeric_infix,
|
Node.NODE_NUMERIC_INFIX: do_node_numeric_infix,
|
||||||
|
Node.NODE_ARGUMENTS: do_node_arguments,
|
||||||
|
Node.NODE_CALL: do_node_call,
|
||||||
}
|
}
|
||||||
|
|
||||||
def expr(self, prog):
|
def expr(self, prog):
|
||||||
@ -536,7 +620,7 @@ class TemplateFormatter(string.Formatter):
|
|||||||
lex_scanner = re.Scanner([
|
lex_scanner = re.Scanner([
|
||||||
(r'(==#|!=#|<=#|<#|>=#|>#)', lambda x,t: (_Parser.LEX_NUMERIC_INFIX, t)),
|
(r'(==#|!=#|<=#|<#|>=#|>#)', lambda x,t: (_Parser.LEX_NUMERIC_INFIX, t)),
|
||||||
(r'(==|!=|<=|<|>=|>)', lambda x,t: (_Parser.LEX_STRING_INFIX, t)), # noqa
|
(r'(==|!=|<=|<|>=|>)', lambda x,t: (_Parser.LEX_STRING_INFIX, t)), # noqa
|
||||||
(r'(if|then|else|fi)\b', lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
(r'(if|then|else|fi|call)\b',lambda x,t: (_Parser.LEX_KEYWORD, t)), # noqa
|
||||||
(r'[(),=;]', lambda x,t: (_Parser.LEX_OP, t)), # noqa
|
(r'[(),=;]', lambda x,t: (_Parser.LEX_OP, t)), # noqa
|
||||||
(r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa
|
(r'-?[\d\.]+', lambda x,t: (_Parser.LEX_CONST, t)), # noqa
|
||||||
(r'\$', lambda x,t: (_Parser.LEX_ID, t)), # noqa
|
(r'\$', lambda x,t: (_Parser.LEX_ID, t)), # noqa
|
||||||
@ -554,10 +638,10 @@ class TemplateFormatter(string.Formatter):
|
|||||||
if column_name is not None and self.template_cache is not None:
|
if column_name is not None and self.template_cache is not None:
|
||||||
tree = self.template_cache.get(column_name, None)
|
tree = self.template_cache.get(column_name, None)
|
||||||
if not tree:
|
if not tree:
|
||||||
tree = self.gpm_parser.program(self.funcs, self.lex_scanner.scan(prog))
|
tree = self.gpm_parser.program(self, self.funcs, self.lex_scanner.scan(prog))
|
||||||
self.template_cache[column_name] = tree
|
self.template_cache[column_name] = tree
|
||||||
else:
|
else:
|
||||||
tree = self.gpm_parser.program(self.funcs, self.lex_scanner.scan(prog))
|
tree = self.gpm_parser.program(self, self.funcs, self.lex_scanner.scan(prog))
|
||||||
return self.gpm_interpreter.program(self.funcs, self, tree, val)
|
return self.gpm_interpreter.program(self.funcs, self, tree, val)
|
||||||
|
|
||||||
# ################# Override parent classes methods #####################
|
# ################# Override parent classes methods #####################
|
||||||
|
@ -126,6 +126,7 @@ class FormatterFunction(object):
|
|||||||
category = 'Unknown'
|
category = 'Unknown'
|
||||||
arg_count = 0
|
arg_count = 0
|
||||||
aliases = []
|
aliases = []
|
||||||
|
is_python = True
|
||||||
|
|
||||||
def evaluate(self, formatter, kwargs, mi, locals, *args):
|
def evaluate(self, formatter, kwargs, mi, locals, *args):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@ -1813,17 +1814,35 @@ _formatter_builtins = [
|
|||||||
|
|
||||||
class FormatterUserFunction(FormatterFunction):
|
class FormatterUserFunction(FormatterFunction):
|
||||||
|
|
||||||
def __init__(self, name, doc, arg_count, program_text):
|
def __init__(self, name, doc, arg_count, program_text, is_python):
|
||||||
|
self.is_python = is_python
|
||||||
self.name = name
|
self.name = name
|
||||||
self.doc = doc
|
self.doc = doc
|
||||||
self.arg_count = arg_count
|
self.arg_count = arg_count
|
||||||
self.program_text = program_text
|
self.program_text = program_text
|
||||||
|
|
||||||
|
def to_pref(self):
|
||||||
|
return [self.name, self.doc, self.arg_count, self.program_text]
|
||||||
|
|
||||||
tabs = re.compile(r'^\t*')
|
tabs = re.compile(r'^\t*')
|
||||||
|
|
||||||
|
|
||||||
|
def function_pref_is_python(pref):
|
||||||
|
if isinstance(pref, list):
|
||||||
|
pref = pref[3]
|
||||||
|
if pref.startswith('def'):
|
||||||
|
return True
|
||||||
|
if pref.startswith('program'):
|
||||||
|
return False
|
||||||
|
raise ValueError('Unknown program type in formatter function pref')
|
||||||
|
|
||||||
|
def function_pref_name(pref):
|
||||||
|
return pref[0]
|
||||||
|
|
||||||
def compile_user_function(name, doc, arg_count, eval_func):
|
def compile_user_function(name, doc, arg_count, eval_func):
|
||||||
|
if not function_pref_is_python(eval_func):
|
||||||
|
return FormatterUserFunction(name, doc, arg_count, eval_func, False)
|
||||||
|
|
||||||
def replace_func(mo):
|
def replace_func(mo):
|
||||||
return mo.group().replace('\t', ' ')
|
return mo.group().replace('\t', ' ')
|
||||||
|
|
||||||
@ -1838,7 +1857,7 @@ class UserFunction(FormatterUserFunction):
|
|||||||
if DEBUG and tweaks.get('enable_template_debug_printing', False):
|
if DEBUG and tweaks.get('enable_template_debug_printing', False):
|
||||||
print(prog)
|
print(prog)
|
||||||
exec(prog, locals_)
|
exec(prog, locals_)
|
||||||
cls = locals_['UserFunction'](name, doc, arg_count, eval_func)
|
cls = locals_['UserFunction'](name, doc, arg_count, eval_func, True)
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
@ -1855,6 +1874,7 @@ def compile_user_template_functions(funcs):
|
|||||||
# then white space differences don't cause them to compare differently
|
# then white space differences don't cause them to compare differently
|
||||||
|
|
||||||
cls = compile_user_function(*func)
|
cls = compile_user_function(*func)
|
||||||
|
cls.is_python = function_pref_is_python(func)
|
||||||
compiled_funcs[cls.name] = cls
|
compiled_funcs[cls.name] = cls
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
@ -1862,7 +1882,7 @@ def compile_user_template_functions(funcs):
|
|||||||
except Exception:
|
except Exception:
|
||||||
func_name = 'Unknown'
|
func_name = 'Unknown'
|
||||||
prints('**** Compilation errors in user template function "%s" ****' % func_name)
|
prints('**** Compilation errors in user template function "%s" ****' % func_name)
|
||||||
traceback.print_exc(limit=0)
|
traceback.print_exc(limit=10)
|
||||||
prints('**** End compilation errors in %s "****"' % func_name)
|
prints('**** End compilation errors in %s "****"' % func_name)
|
||||||
return compiled_funcs
|
return compiled_funcs
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user