From 8204c8062e1bc5f096fd3dd55b2131fca316b3a1 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Wed, 13 Nov 2024 13:31:02 +0000 Subject: [PATCH] The documentation editing tool with changes to support it. It is an action so it can be opened easily, should a document writer want to use it. It is also available in the Stored templates / User functions dialog. It is possible that no one other than me will use it. :) --- src/calibre/customize/builtins.py | 9 +- src/calibre/gui2/actions/ff_doc_assistant.py | 27 +++ src/calibre/gui2/dialogs/ff_doc_editor.py | 187 ++++++++++++++++++ src/calibre/gui2/dialogs/template_dialog.py | 2 +- .../gui2/preferences/template_functions.py | 19 ++ .../gui2/preferences/template_functions.ui | 72 +++++-- src/calibre/utils/ffml_processor.py | 2 +- src/calibre/utils/formatter_functions.py | 12 +- 8 files changed, 315 insertions(+), 15 deletions(-) create mode 100644 src/calibre/gui2/actions/ff_doc_assistant.py create mode 100644 src/calibre/gui2/dialogs/ff_doc_editor.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 52ea221a46cd..fc7bd0ad8a71 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1131,6 +1131,13 @@ class ActionBooklistContextMenu(InterfaceActionBase): description = _('Open the context menu for the column') +class ActionFFDocAsst(InterfaceActionBase): + name = 'Template function document edit' + author = 'Charles Haley' + actual_plugin = 'calibre.gui2.actions.ff_doc_assistant:FFDocEditAssistantAction' + description = _('Open a template function documentation editor') + + class ActionAllActions(InterfaceActionBase): name = 'All GUI actions' author = 'Charles Haley' @@ -1179,7 +1186,7 @@ class ActionPluginUpdater(InterfaceActionBase): ActionMarkBooks, ActionEmbed, ActionTemplateTester, ActionTagMapper, ActionAuthorMapper, ActionVirtualLibrary, ActionBrowseAnnotations, ActionTemplateFunctions, ActionAutoscrollBooks, ActionFullTextSearch, ActionManageCategories, ActionBooklistContextMenu, ActionSavedSearches, - ActionLayoutActions, ActionBrowseNotes,] + ActionLayoutActions, ActionBrowseNotes, ActionFFDocAsst,] # }}} diff --git a/src/calibre/gui2/actions/ff_doc_assistant.py b/src/calibre/gui2/actions/ff_doc_assistant.py new file mode 100644 index 000000000000..bbae6e3f0dc7 --- /dev/null +++ b/src/calibre/gui2/actions/ff_doc_assistant.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2022, Charles Haley +# + +from qt.core import QMenu, QToolButton + +from calibre.gui2.actions import InterfaceAction +from calibre.gui2.dialogs.ff_doc_editor import FFDocEditor + + +class FFDocEditAssistantAction(InterfaceAction): + + name = 'Edit template function documents' + action_spec = (_('Edit template function documents'), 'edit_input.png', + _('Template function documentation editing assistant'), ()) + action_type = 'current' + # popup_type = QToolButton.ToolButtonPopupMode.InstantPopup + # action_add_menu = True + dont_add_to = frozenset(['context-menu-device', 'menubar-device']) + + def genesis(self): + self.menu = m = self.qaction.menu() + self.qaction.triggered.connect(self.show_editor) + + def show_editor(self): + d = FFDocEditor(can_copy_back=False) + d.exec() \ No newline at end of file diff --git a/src/calibre/gui2/dialogs/ff_doc_editor.py b/src/calibre/gui2/dialogs/ff_doc_editor.py new file mode 100644 index 000000000000..dbd0cd4eaa78 --- /dev/null +++ b/src/calibre/gui2/dialogs/ff_doc_editor.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python + + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +''' +Created on 12 Nov 2024 + +@author: chaley +''' + +from qt.core import (QApplication, QCheckBox, QComboBox, QFrame, QLabel, QGridLayout, + QHBoxLayout, QPlainTextEdit, QPushButton, QSize, QTimer) + +from calibre.constants import iswindows +from calibre.gui2 import gprefs +from calibre.gui2.widgets2 import Dialog, HTMLDisplay +from calibre.utils.ffml_processor import FFMLProcessor +from calibre.utils.formatter_functions import formatter_functions + + +class FFDocEditor(Dialog): + + def __init__(self, can_copy_back=False, parent=None): + self.ffml = FFMLProcessor() + self.can_copy_back = can_copy_back + self.last_operation = None + super().__init__(title=_('Template function documentation editor'), + name='template_function_doc_editor_dialog', parent=parent) + + def sizeHint(self): + return QSize(800, 600) + + def set_document_text(self, text): + self.editable_text_widget.setPlainText(text) + + def document_text(self): + return self.editable_text_widget.toPlainText() + + def copy_text(self): + QApplication.instance().clipboard().setText(self.document_text()) + + def html_widget(self, layout, row, column): + e = HTMLDisplay() + e.setFrameStyle(QFrame.Shape.Box) + if iswindows: + e.setDefaultStyleSheet('pre { font-family: "Segoe UI Mono", "Consolas", monospace; }') + layout.addWidget(e, row, column, 1, 1) + return e + + def text_widget(self, read_only, layout, row, column): + e = QPlainTextEdit() + e.setReadOnly(read_only) + e.setFrameStyle(QFrame.Shape.Box) + layout.addWidget(e, row, column, 1, 1) + return e + + def label_widget(self, text, layout, row, column, colspan=None): + e = QLabel(text) + layout.addWidget(e, row, column, 1, colspan if colspan is not None else 1) + return e + + def setup_ui(self): + gl = QGridLayout(self) + hl = QHBoxLayout() + + so = self.show_original_cb = QCheckBox(_('Show documentation for function')) + so.setChecked(gprefs.get('template_function_doc_editor_show_original', False)) + so.stateChanged.connect(self.first_row_checkbox_changed) + hl.addWidget(so) + + f = self.functions_box = QComboBox() + self.builtins = formatter_functions().get_builtins() + f.addItem('') + f.addItems(self.builtins.keys()) + hl.addWidget(f) + f.currentIndexChanged.connect(self.functions_box_index_changed) + + so = self.show_in_english_cb = QCheckBox(_('Show original English')) + so.stateChanged.connect(self.first_row_checkbox_changed) + hl.addWidget(so) + + so = self.show_formatted_cb = QCheckBox(_('Show with placeholders replaced')) + so.stateChanged.connect(self.first_row_checkbox_changed) + hl.addWidget(so) + + hl.addStretch() + gl.addLayout(hl, 0, 0, 1, 2) + + self.original_doc_label = self.label_widget( + _('Raw documentation for the selected function'), gl, 1, 0) + w = self.original_doc_html_label = self.label_widget( + _('Documentation for the selected function in HTML'), gl, 1, 1) + w.setVisible(so.isChecked()) + w = self.original_text_widget = self.text_widget(True, gl, 2, 0) + w.setVisible(so.isChecked()) + w = self.original_text_result = self.html_widget(gl, 2, 1) + w.setVisible(so.isChecked()) + + self.label_widget(_('Document being edited'), gl, 3, 0) + l = QHBoxLayout() + l.addWidget(QLabel(_('Document in HTML'))) + cb = self.doc_show_formatted_cb = QCheckBox(_('Show with placeholders replaced')) + cb.setToolTip(_('This requires the original function documentation to be visible above')) + cb.stateChanged.connect(self._editable_box_changed) + l.addWidget(cb) + l.addStretch() + gl.addLayout(l, 3, 1) + + w = self.editable_text_widget = self.text_widget(False, gl, 4, 0) + w.textChanged.connect(self.editable_box_changed) + self.editable_text_result = self.html_widget(gl, 4, 1) + if self.can_copy_back: + self.label_widget(_('Text will be stored with the saved template/function'), gl, 5, 0) + else: + self.label_widget(_('You must copy the text then paste it where it is needed'), gl, 5, 0, colspan=2) + + l = QHBoxLayout() + b = QPushButton(_('&Copy text')) + b.clicked.connect(self.copy_text) + l.addWidget(b) + l.addStretch() + gl.addLayout(l, 6, 0) + gl.addWidget(self.bb, 6, 1) + + self.changed_timer = QTimer() + self.fill_in_top_row() + + def editable_box_changed(self): + self.changed_timer.stop() + t = self.changed_timer = QTimer() + t.timeout.connect(self._editable_box_changed) + t.setSingleShot(True) + t.setInterval(250) + t.start() + + def _editable_box_changed(self): + name = self.functions_box.currentText() + if name and self.doc_show_formatted_cb.isVisible() and self.doc_show_formatted_cb.isChecked(): + doc = self.builtins[name].doc + self.editable_text_result.setHtml( + self.ffml.document_to_html(doc.format_again( + self.editable_text_widget.toPlainText()), 'edited text')) + else: + self.editable_text_result.setHtml( + self.ffml.document_to_html(self.editable_text_widget.toPlainText(), 'edited text')) + + def fill_in_top_row(self): + to_show = self.show_original_cb.isChecked() + self.original_doc_label.setVisible(to_show) + self.original_doc_html_label.setVisible(to_show) + self.show_in_english_cb.setVisible(to_show) + self.show_formatted_cb.setVisible(to_show) + self.original_text_widget.setVisible(to_show) + self.original_text_result.setVisible(to_show) + if not to_show: + self.doc_show_formatted_cb.setVisible(False) + self._editable_box_changed() + return + name = self.functions_box.currentText() + if name in self.builtins: + doc = self.builtins[name].doc + if not self.can_copy_back: + self.doc_show_formatted_cb.setVisible(True) + if self.show_in_english_cb.isChecked(): + html = doc.formatted_english if self.show_formatted_cb.isChecked() else doc.raw_english + self.original_text_widget.setPlainText(doc.raw_english.lstrip()) + self.original_text_result.setHtml(self.ffml.document_to_html(html, name)) + else: + html = doc.formatted_other if self.show_formatted_cb.isChecked() else doc.raw_other + self.original_text_widget.setPlainText(doc.raw_other.lstrip()) + self.original_text_result.setHtml(self.ffml.document_to_html(html, name)) + else: + self.original_text_widget.setPlainText('') + self.original_text_result.setHtml(self.ffml.document_to_html('', name)) + self.doc_show_formatted_cb.setVisible(False) + self._editable_box_changed() + + def first_row_checkbox_changed(self): + gprefs['template_function_doc_editor_show_original'] = self.show_original_cb.isChecked() + self.fill_in_top_row() + + def functions_box_index_changed(self, idx): + self.show_original_cb.setChecked(True) + self.fill_in_top_row() diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 2c2b053a4578..1d507b3e3395 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -119,7 +119,7 @@ def show_all_functions(self): self.last_operation = self.show_all_functions result = [] a = result.append - for name in sorted(self.builtins): + for name in sorted(self.builtins, key=sort_key): a(self.header_line(name)) try: doc = self.get_doc(self.builtins[name]) diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py index 68fab7cb73a7..27d213c1636d 100644 --- a/src/calibre/gui2/preferences/template_functions.py +++ b/src/calibre/gui2/preferences/template_functions.py @@ -8,6 +8,7 @@ from qt.core import QDialog, QDialogButtonBox from calibre.gui2 import error_dialog, gprefs, question_dialog, warning_dialog +from calibre.gui2.dialogs.ff_doc_editor import FFDocEditor 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 @@ -187,6 +188,7 @@ def initialize(self): 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.doc_edit_button.clicked.connect(self.doc_edit_button_clicked) self.create_button.setEnabled(False) self.delete_button.setEnabled(False) self.replace_button.setEnabled(False) @@ -206,9 +208,11 @@ def initialize(self): self.st_delete_button.setEnabled(False) self.st_replace_button.setEnabled(False) self.st_test_template_button.setEnabled(False) + self.st_doc_edit_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_doc_edit_button.clicked.connect(self.st_doc_edit_button_clicked) self.st_current_program_name = '' self.st_current_program_text = '' @@ -239,6 +243,12 @@ def show_only_user_defined_changed(self, state): def enable_replace_button(self): self.replace_button.setEnabled(self.delete_button.isEnabled()) + def doc_edit_button_clicked(self): + d = FFDocEditor(can_copy_back=True, parent=self) + d.set_document_text(self.documentation.toPlainText()) + if d.exec() == QDialog.DialogCode.Accepted: + self.documentation.setPlainText(d.document_text()) + def clear_button_clicked(self): self.build_function_names_box() self.program.clear() @@ -425,6 +435,7 @@ def st_clear_button_clicked(self): self.template_editor.new_doc.clear() self.st_create_button.setEnabled(False) self.st_delete_button.setEnabled(False) + self.st_doc_edit_button.setEnabled(False) def st_build_function_names_box(self, scroll_to=''): self.te_name.blockSignals(True) @@ -447,6 +458,7 @@ def st_delete_button_clicked(self): self.changed_signal.emit() self.st_create_button.setEnabled(True) self.st_delete_button.setEnabled(False) + self.st_doc_edit_button.setEnabled(False) self.st_build_function_names_box() self.te_textbox.setReadOnly(False) self.st_current_program_name = '' @@ -483,6 +495,7 @@ def st_template_name_edited(self, txt): self.st_delete_button.setEnabled(b) self.st_test_template_button.setEnabled(b) self.te_textbox.setReadOnly(False) + self.st_doc_edit_button.setEnabled(True) def st_function_index_changed(self, idx): txt = self.te_name.currentText() @@ -518,6 +531,12 @@ def st_replace_button_clicked(self): self.st_delete_button_clicked() self.st_create_button_clicked(use_name=name) + def st_doc_edit_button_clicked(self): + d = FFDocEditor(can_copy_back=True, parent=self) + d.set_document_text(self.template_editor.new_doc.toPlainText()) + if d.exec() == QDialog.DialogCode.Accepted: + self.template_editor.new_doc.setPlainText(d.document_text()) + def commit(self): pref_value = [] for name, cls in iteritems(self.funcs): diff --git a/src/calibre/gui2/preferences/template_functions.ui b/src/calibre/gui2/preferences/template_functions.ui index 4c8690da2dde..df2a4bbaf0d6 100644 --- a/src/calibre/gui2/preferences/template_functions.ui +++ b/src/calibre/gui2/preferences/template_functions.ui @@ -81,6 +81,29 @@ + + + + Doc Editor + + + Open an editor for function documentation + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + @@ -185,17 +208,44 @@ a new related user defined function.</p> - - - D&ocumentation: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - documentation - - + + + + + D&ocumentation: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + documentation + + + + + + + Doc Editor + + + Open an editor for function documentation + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + diff --git a/src/calibre/utils/ffml_processor.py b/src/calibre/utils/ffml_processor.py index 35c0fdfc1e7b..cf948bbe957a 100644 --- a/src/calibre/utils/ffml_processor.py +++ b/src/calibre/utils/ffml_processor.py @@ -253,7 +253,7 @@ def parse_document(self, doc, name): self.document_name = name node = DocumentNode() - return self._parse_document(node) + return self._parse_document(node) if doc else node def tree_to_html(self, tree, depth=0): """ diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 7665a38fd5dc..668ab67f940c 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -49,13 +49,23 @@ def __new__(cls, raw_english, raw_other, formatted_english, formatted_other): instance.raw_other = raw_other instance.formatted_english = formatted_english instance.formatted_other = formatted_other + instance.did_format = False return instance def format(self, *args, **kw): formatted_english = self.raw_english.format(*args, **kw) formatted_other = self.raw_other.format(*args, **kw) - return TranslatedStringWithRaw(self.raw_english, self.raw_other, + v = TranslatedStringWithRaw(self.raw_english, self.raw_other, formatted_english, formatted_other) + v.saved_args = args + v.saved_kwargs = kw + v.did_format = True + return v + + def format_again(self, txt): + if self.did_format: + return txt.format(*self.saved_args, **self.saved_kwargs) + return txt def _(txt):