This commit is contained in:
Kovid Goyal 2021-04-06 14:29:59 +05:30
commit baad228936
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 179 additions and 89 deletions

View File

@ -35,7 +35,7 @@ class ShowTemplateTesterAction(InterfaceAction):
rows = view.selectionModel().selectedRows()
if not rows:
return error_dialog(self.gui, _('No books selected'),
_('One book must be selected'), show=True)
_('At least one book must be selected'), show=True)
mi = []
db = view.model().db
for row in rows:

View File

@ -246,6 +246,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.global_vars = global_vars
cols = []
self.fm = fm
if fm is not None:
for key in sorted(displayable_columns(fm),
key=lambda k: sort_key(fm[k]['name'] if k != color_row_key else 0)):
@ -305,66 +306,6 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.template_name_label.setVisible(False)
self.template_name.setVisible(False)
if mi:
if not isinstance(mi, list):
mi = (mi, )
else:
mi = Metadata(_('Title'), [_('Author')])
mi.author_sort = _('Author Sort')
mi.series = ngettext('Series', 'Series', 1)
mi.series_index = 3
mi.rating = 4.0
mi.tags = [_('Tag 1'), _('Tag 2')]
mi.languages = ['eng']
mi.id = 1
if fm is not None:
mi.set_all_user_metadata(fm.custom_field_metadata())
else:
# No field metadata. Grab a copy from the current library so
# that we can validate any custom column names. The values for
# the columns will all be empty, which in some very unusual
# cases might cause formatter errors. We can live with that.
from calibre.gui2.ui import get_gui
mi.set_all_user_metadata(
get_gui().current_db.new_api.field_metadata.custom_field_metadata())
for col in mi.get_all_user_metadata(False):
mi.set(col, (col,), 0)
mi = (mi, )
self.mi = mi
# Set up the display table
self.table_column_widths = None
try:
self.table_column_widths = \
gprefs.get('template_editor_table_widths', None)
except:
pass
tv = self.template_value
tv.setRowCount(len(mi))
tv.setColumnCount(2)
tv.setHorizontalHeaderLabels((_('Book title'), _('Template value')))
tv.horizontalHeader().setStretchLastSection(True)
tv.horizontalHeader().sectionResized.connect(self.table_column_resized)
# Set the height of the table
h = tv.rowHeight(0) * min(len(mi), 5)
h += 2 * tv.frameWidth() + tv.horizontalHeader().height()
tv.setMinimumHeight(h)
tv.setMaximumHeight(h)
# Set the size of the title column
if self.table_column_widths:
tv.setColumnWidth(0, self.table_column_widths[0])
else:
tv.setColumnWidth(0, tv.fontMetrics().averageCharWidth() * 10)
# Use our own widget to get rid of elision. setTextElideMode() doesn't work
for r in range(0, len(mi)):
w = QLineEdit(tv)
w.setReadOnly(True)
tv.setCellWidget(r, 0, w)
w = QLineEdit(tv)
w.setReadOnly(True)
tv.setCellWidget(r, 1, w)
tv.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowType.WindowContextHelpButtonHint))
@ -374,6 +315,15 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.builtins = (builtin_functions if builtin_functions else
formatter_functions().get_builtins_and_aliases())
# Set up the display table
self.table_column_widths = None
try:
self.table_column_widths = \
gprefs.get('template_editor_table_widths', None)
except:
pass
self.set_mi(mi, fm)
self.last_text = ''
self.highlighter = TemplateHighlighter(self.textbox.document(), builtin_functions=self.builtins)
self.textbox.cursorPositionChanged.connect(self.text_cursor_changed)
@ -415,7 +365,6 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.function_type_string(f, longform=False)), f)
self.function.setCurrentIndex(0)
self.function.currentIndexChanged.connect(self.function_changed)
self.display_values(text)
self.rule = (None, '')
tt = _('Template language tutorial')
@ -457,6 +406,66 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
except Exception:
pass
def set_mi(self, mi, fm):
'''
This sets the metadata for the test result books table. It doesn't reset
the contents of the field selectors for editing rules.
'''
self.fm = fm
if mi:
if not isinstance(mi, list):
mi = (mi, )
else:
mi = Metadata(_('Title'), [_('Author')])
mi.author_sort = _('Author Sort')
mi.series = ngettext('Series', 'Series', 1)
mi.series_index = 3
mi.rating = 4.0
mi.tags = [_('Tag 1'), _('Tag 2')]
mi.languages = ['eng']
mi.id = 1
if self.fm is not None:
mi.set_all_user_metadata(self.fm.custom_field_metadata())
else:
# No field metadata. Grab a copy from the current library so
# that we can validate any custom column names. The values for
# the columns will all be empty, which in some very unusual
# cases might cause formatter errors. We can live with that.
from calibre.gui2.ui import get_gui
mi.set_all_user_metadata(
get_gui().current_db.new_api.field_metadata.custom_field_metadata())
for col in mi.get_all_user_metadata(False):
mi.set(col, (col,), 0)
mi = (mi, )
self.mi = mi
tv = self.template_value
tv.setColumnCount(2)
tv.setHorizontalHeaderLabels((_('Book title'), _('Template value')))
tv.horizontalHeader().setStretchLastSection(True)
tv.horizontalHeader().sectionResized.connect(self.table_column_resized)
tv.setRowCount(len(mi))
# Set the height of the table
h = tv.rowHeight(0) * min(len(mi), 5)
h += 2 * tv.frameWidth() + tv.horizontalHeader().height()
tv.setMinimumHeight(h)
tv.setMaximumHeight(h)
# Set the size of the title column
if self.table_column_widths:
tv.setColumnWidth(0, self.table_column_widths[0])
else:
tv.setColumnWidth(0, tv.fontMetrics().averageCharWidth() * 10)
tv.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
tv.setRowCount(len(mi))
# Use our own widget to get rid of elision. setTextElideMode() doesn't work
for r in range(0, len(mi)):
w = QLineEdit(tv)
w.setReadOnly(True)
tv.setCellWidget(r, 0, w)
w = QLineEdit(tv)
w.setReadOnly(True)
tv.setCellWidget(r, 1, w)
self.display_values('')
def show_context_menu(self, point):
m = self.textbox.createStandardContextMenu()
m.addSeparator()

View File

@ -2,11 +2,11 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net>
import json
import traceback
from qt.core import QDialogButtonBox
import copy, json, traceback
from qt.core import QDialogButtonBox, QDialog
from calibre.gui2 import error_dialog, warning_dialog
from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.template_functions_ui import Ui_Form
from calibre.gui2.widgets import PythonHighlighter
@ -121,21 +121,43 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
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.template_editor.template_name.currentIndexChanged[native_string_type].connect(self.st_function_index_changed)
self.template_editor.template_name.editTextChanged.connect(self.st_template_name_edited)
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)
# Python funtion tab
# 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[0], self.fm)
# Python function tab
def enable_replace_button(self):
self.replace_button.setEnabled(self.delete_button.isEnabled())
@ -197,7 +219,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
_('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=False, show_copy_button=False)
box.bb.setStandardButtons(box.bb.standardButtons() | QDialogButtonBox.StandardButton.Cancel)
box.det_msg_toggle.setVisible(False)
if not box.exec_():
@ -259,48 +281,66 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
# 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.template_editor.textbox.clear()
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.template_editor.template_name.blockSignals(True)
self.te_name.blockSignals(True)
func_names = sorted(self.st_funcs)
self.template_editor.template_name.clear()
self.template_editor.template_name.addItem('')
self.template_editor.template_name.addItems(func_names)
self.template_editor.template_name.setCurrentIndex(0)
self.template_editor.template_name.blockSignals(False)
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.template_editor.template_name.findText(scroll_to)
idx = self.te_name.findText(scroll_to)
if idx >= 0:
self.template_editor.template_name.setCurrentIndex(idx)
self.te_name.setCurrentIndex(idx)
def st_delete_button_clicked(self):
name = unicode_type(self.template_editor.template_name.currentText())
name = unicode_type(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.template_editor.textbox.setReadOnly(False)
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 unicode_type(self.template_editor.template_name.currentText())
name = use_name if use_name else unicode_type(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 = unicode_type(self.template_editor.textbox.toPlainText())
prog = unicode_type(self.te_textbox.toPlainText())
if not prog.startswith('program:'):
error_dialog(self.gui, _('Stored templates'),
_('The stored template must begin with "program:"'), show=True)
@ -319,22 +359,40 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.st_create_button.setEnabled(not b)
self.st_replace_button.setEnabled(b)
self.st_delete_button.setEnabled(b)
self.template_editor.textbox.setReadOnly(False)
self.st_test_template_button.setEnabled(b)
self.te_textbox.setReadOnly(False)
def st_function_index_changed(self, txt):
txt = unicode_type(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.template_editor.textbox.clear()
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.template_editor.textbox.setPlainText(func.program_text)
self.te_textbox.setPlainText(func.program_text)
self.st_template_name_edited(txt)
def st_replace_button_clicked(self):
name = unicode_type(self.template_editor.template_name.currentText())
name = unicode_type(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)

View File

@ -80,6 +80,29 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="st_test_template_button">
<property name="text">
<string>Test</string>
</property>
<property name="toolTip">
<string>Open a template tester dialog to use a template to test stored templates</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>

View File

@ -91,8 +91,8 @@ class FunctionNode(Node):
class CallNode(Node):
def __init__(self, line_number, function, expression_list):
Node.__init__(self, line_number, 'call template: ' + function)
def __init__(self, line_number, name, function, expression_list):
Node.__init__(self, line_number, 'call template: ' + name)
self.node_type = self.NODE_CALL
self.function = function
self.expression_list = expression_list
@ -635,7 +635,7 @@ class _Parser(object):
subprog = _Parser().program(self, self.funcs,
self.parent.lex_scanner.scan(text))
self.funcs[name].cached_parse_tree = subprog
return CallNode(self.line_number, subprog, arguments)
return CallNode(self.line_number, name, subprog, arguments)
def expr(self):
if self.token_op_is_lparen():