diff --git a/src/calibre/gui2/actions/show_template_tester.py b/src/calibre/gui2/actions/show_template_tester.py index 3060ad4a21..1f8b98bc5a 100644 --- a/src/calibre/gui2/actions/show_template_tester.py +++ b/src/calibre/gui2/actions/show_template_tester.py @@ -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: diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index de8a90b7a8..0dc541d827 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -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() diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py index 6c7f63dd46..c09d2eee1b 100644 --- a/src/calibre/gui2/preferences/template_functions.py +++ b/src/calibre/gui2/preferences/template_functions.py @@ -2,11 +2,11 @@ # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # License: GPLv3 Copyright: 2010, Kovid Goyal -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) diff --git a/src/calibre/gui2/preferences/template_functions.ui b/src/calibre/gui2/preferences/template_functions.ui index 22b24555ca..d8961fdfb4 100644 --- a/src/calibre/gui2/preferences/template_functions.ui +++ b/src/calibre/gui2/preferences/template_functions.ui @@ -80,6 +80,29 @@ + + + + Test + + + Open a template tester dialog to use a template to test stored templates + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 150ad79e57..6f6bd45863 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -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():