finalcif 159__tar.gz → 161__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. finalcif-161/.githooks/pre-push +39 -0
  2. {finalcif-159 → finalcif-161}/.run/compile_ui_files.run.xml +1 -1
  3. finalcif-161/AGENTS.md +58 -0
  4. {finalcif-159 → finalcif-161}/PKG-INFO +5 -5
  5. {finalcif-159 → finalcif-161}/README.md +2 -2
  6. finalcif-161/finalcif/__init__.py +3 -0
  7. {finalcif-159 → finalcif-161}/finalcif/appwindow.py +48 -6
  8. {finalcif-159 → finalcif-161}/finalcif/cif/text.py +86 -51
  9. {finalcif-159 → finalcif-161}/finalcif/equip_property/author_loop_templates.py +88 -11
  10. {finalcif-159 → finalcif-161}/finalcif/equip_property/equipment.py +73 -1
  11. {finalcif-159 → finalcif-161}/finalcif/equip_property/properties.py +60 -1
  12. {finalcif-159 → finalcif-161}/finalcif/report/templated_report.py +109 -12
  13. finalcif-161/finalcif/template/report.tex +240 -0
  14. {finalcif-159 → finalcif-161}/finalcif/template/report.tmpl +4 -3
  15. finalcif-161/finalcif/template/report2.tex +249 -0
  16. finalcif-161/finalcif/template/template_text.docx +0 -0
  17. finalcif-161/finalcif/template/template_without_text.docx +0 -0
  18. {finalcif-159 → finalcif-161}/finalcif/template/templates.py +4 -3
  19. finalcif-161/finalcif/template/unicode2latex/FakePlasTeX/FakeContext.py +77 -0
  20. finalcif-161/finalcif/template/unicode2latex/FakePlasTeX/FakeTokenizer.py +493 -0
  21. finalcif-161/finalcif/template/unicode2latex/FakePlasTeX/README.md +4 -0
  22. finalcif-161/finalcif/template/unicode2latex/FakePlasTeX/TokenizerPassThru.py +190 -0
  23. finalcif-161/finalcif/template/unicode2latex/LICENSE +25 -0
  24. finalcif-161/finalcif/template/unicode2latex/u2l.py +647 -0
  25. finalcif-161/finalcif/template/unicode2latex/unicode-math-table.tex +2469 -0
  26. finalcif-161/finalcif/template/unicode2latex/unicode-math-xetex.sty +3882 -0
  27. {finalcif-159 → finalcif-161}/finalcif/template/xsl/convert.py +24 -2
  28. finalcif-161/finalcif/template/xsl/xmltolatex.xsl +85 -0
  29. finalcif-161/finalcif/tools/__init__.py +0 -0
  30. {finalcif-159 → finalcif-161}/finalcif/tools/options.py +12 -0
  31. {finalcif-159 → finalcif-161}/finalcif/tools/settings.py +27 -1
  32. {finalcif-159 → finalcif-161}/finalcif/tools/space_groups.py +21 -0
  33. {finalcif-159 → finalcif-161}/pyproject.toml +3 -3
  34. {finalcif-159 → finalcif-161}/uv.lock +101 -85
  35. finalcif-159/AGENTS.md +0 -31
  36. finalcif-159/finalcif/__init__.py +0 -2
  37. finalcif-159/finalcif/template/template_text.docx +0 -0
  38. finalcif-159/finalcif/template/template_without_text.docx +0 -0
  39. {finalcif-159 → finalcif-161}/.gitattributes +0 -0
  40. {finalcif-159 → finalcif-161}/.gitignore +0 -0
  41. {finalcif-159 → finalcif-161}/.python-version +0 -0
  42. {finalcif-159 → finalcif-161}/.ruff.toml +0 -0
  43. {finalcif-159 → finalcif-161}/.run/Make Documentation MAC.run.xml +0 -0
  44. {finalcif-159 → finalcif-161}/.run/make documentation Windows.run.xml +0 -0
  45. {finalcif-159 → finalcif-161}/CODE_OF_CONDUCT.md +0 -0
  46. {finalcif-159 → finalcif-161}/CONTRIBUTING.md +0 -0
  47. {finalcif-159 → finalcif-161}/Finalcif_installer_win.spec +0 -0
  48. {finalcif-159 → finalcif-161}/Finalcif_linux_onefile.spec +0 -0
  49. {finalcif-159 → finalcif-161}/Finalcif_mac.spec +0 -0
  50. {finalcif-159 → finalcif-161}/LICENSE +0 -0
  51. {finalcif-159 → finalcif-161}/finalcif/app_path.py +0 -0
  52. {finalcif-159 → finalcif-161}/finalcif/cif/__init__.py +0 -0
  53. {finalcif-159 → finalcif-161}/finalcif/cif/all_cif_dicts.py +0 -0
  54. {finalcif-159 → finalcif-161}/finalcif/cif/atoms.py +0 -0
  55. {finalcif-159 → finalcif-161}/finalcif/cif/checkcif/__init__.py +0 -0
  56. {finalcif-159 → finalcif-161}/finalcif/cif/checkcif/checkcif.py +0 -0
  57. {finalcif-159 → finalcif-161}/finalcif/cif/cif_dict_foo.py +0 -0
  58. {finalcif-159 → finalcif-161}/finalcif/cif/cif_file_io.py +0 -0
  59. {finalcif-159 → finalcif-161}/finalcif/cif/cif_order.py +0 -0
  60. {finalcif-159 → finalcif-161}/finalcif/cif/cod/__init__.py +0 -0
  61. {finalcif-159 → finalcif-161}/finalcif/cif/cod/deposit.py +0 -0
  62. {finalcif-159 → finalcif-161}/finalcif/cif/cod/deposit_check.py +0 -0
  63. {finalcif-159 → finalcif-161}/finalcif/cif/cod/deposition_list.py +0 -0
  64. {finalcif-159 → finalcif-161}/finalcif/cif/cod/doi.py +0 -0
  65. {finalcif-159 → finalcif-161}/finalcif/cif/cod/upload.py +0 -0
  66. {finalcif-159 → finalcif-161}/finalcif/cif/cod/website_parser.py +0 -0
  67. {finalcif-159 → finalcif-161}/finalcif/cif/core_dict.py +0 -0
  68. {finalcif-159 → finalcif-161}/finalcif/cif/hkl.py +0 -0
  69. {finalcif-159 → finalcif-161}/finalcif/cif/modulation_dict.py +0 -0
  70. {finalcif-159 → finalcif-161}/finalcif/cif/powder_dict.py +0 -0
  71. {finalcif-159 → finalcif-161}/finalcif/cif/reference.py +0 -0
  72. {finalcif-159 → finalcif-161}/finalcif/cif/restraints_dict.py +0 -0
  73. {finalcif-159 → finalcif-161}/finalcif/cif/twin_dict.py +0 -0
  74. {finalcif-159 → finalcif-161}/finalcif/cif/vrf_entry.py +0 -0
  75. {finalcif-159 → finalcif-161}/finalcif/ciforder/__init__.py +0 -0
  76. {finalcif-159 → finalcif-161}/finalcif/ciforder/order.py +0 -0
  77. {finalcif-159 → finalcif-161}/finalcif/ciforder/order_ui.py +0 -0
  78. {finalcif-159 → finalcif-161}/finalcif/ciforder/order_ui.ui +0 -0
  79. {finalcif-159 → finalcif-161}/finalcif/datafiles/__init__.py +0 -0
  80. {finalcif-159 → finalcif-161}/finalcif/datafiles/bruker_data.py +0 -0
  81. {finalcif-159 → finalcif-161}/finalcif/datafiles/bruker_frame.py +0 -0
  82. {finalcif-159 → finalcif-161}/finalcif/datafiles/ccdc_mail.py +0 -0
  83. {finalcif-159 → finalcif-161}/finalcif/datafiles/data.py +0 -0
  84. {finalcif-159 → finalcif-161}/finalcif/datafiles/p4p_reader.py +0 -0
  85. {finalcif-159 → finalcif-161}/finalcif/datafiles/sadabs.py +0 -0
  86. {finalcif-159 → finalcif-161}/finalcif/datafiles/saint.py +0 -0
  87. {finalcif-159 → finalcif-161}/finalcif/datafiles/shelx_lst.py +0 -0
  88. {finalcif-159 → finalcif-161}/finalcif/datafiles/utils.py +0 -0
  89. {finalcif-159 → finalcif-161}/finalcif/displaymol/__init__.py +0 -0
  90. {finalcif-159 → finalcif-161}/finalcif/displaymol/vtk_molecule.py +0 -0
  91. {finalcif-159 → finalcif-161}/finalcif/equip_property/__init__.py +0 -0
  92. {finalcif-159 → finalcif-161}/finalcif/equip_property/tools.py +0 -0
  93. {finalcif-159 → finalcif-161}/finalcif/finalcif_start.py +0 -0
  94. {finalcif-159 → finalcif-161}/finalcif/gui/__init__.py +0 -0
  95. {finalcif-159 → finalcif-161}/finalcif/gui/block_combobox.py +0 -0
  96. {finalcif-159 → finalcif-161}/finalcif/gui/cif_table_model.py +0 -0
  97. {finalcif-159 → finalcif-161}/finalcif/gui/cif_table_view.py +0 -0
  98. {finalcif-159 → finalcif-161}/finalcif/gui/combobox.py +0 -0
  99. {finalcif-159 → finalcif-161}/finalcif/gui/custom_classes.py +0 -0
  100. {finalcif-159 → finalcif-161}/finalcif/gui/dialogs.py +0 -0
  101. {finalcif-159 → finalcif-161}/finalcif/gui/edit_button.py +0 -0
  102. {finalcif-159 → finalcif-161}/finalcif/gui/equipmenttable.py +0 -0
  103. {finalcif-159 → finalcif-161}/finalcif/gui/file_editor.py +0 -0
  104. {finalcif-159 → finalcif-161}/finalcif/gui/finalcif_gui_ui.py +0 -0
  105. {finalcif-159 → finalcif-161}/finalcif/gui/finalcif_gui_ui.ui +0 -0
  106. {finalcif-159 → finalcif-161}/finalcif/gui/import_selector.py +0 -0
  107. {finalcif-159 → finalcif-161}/finalcif/gui/import_selector_ui.py +0 -0
  108. {finalcif-159 → finalcif-161}/finalcif/gui/import_selector_ui.ui +0 -0
  109. {finalcif-159 → finalcif-161}/finalcif/gui/loop_creator.py +0 -0
  110. {finalcif-159 → finalcif-161}/finalcif/gui/loop_creator_ui.py +0 -0
  111. {finalcif-159 → finalcif-161}/finalcif/gui/loop_creator_ui.ui +0 -0
  112. {finalcif-159 → finalcif-161}/finalcif/gui/loops.py +0 -0
  113. {finalcif-159 → finalcif-161}/finalcif/gui/mainstackwidget.py +0 -0
  114. {finalcif-159 → finalcif-161}/finalcif/gui/mixins.py +0 -0
  115. {finalcif-159 → finalcif-161}/finalcif/gui/new_key_dialog.py +0 -0
  116. {finalcif-159 → finalcif-161}/finalcif/gui/new_key_dialog_ui.py +0 -0
  117. {finalcif-159 → finalcif-161}/finalcif/gui/new_key_dialog_ui.ui +0 -0
  118. {finalcif-159 → finalcif-161}/finalcif/gui/plaintextedit.py +0 -0
  119. {finalcif-159 → finalcif-161}/finalcif/gui/playground.py +0 -0
  120. {finalcif-159 → finalcif-161}/finalcif/gui/propertytable.py +0 -0
  121. {finalcif-159 → finalcif-161}/finalcif/gui/spell_check_edit.py +0 -0
  122. {finalcif-159 → finalcif-161}/finalcif/gui/syntax_highlighter.py +0 -0
  123. {finalcif-159 → finalcif-161}/finalcif/gui/text_templates_ui.py +0 -0
  124. {finalcif-159 → finalcif-161}/finalcif/gui/text_templates_ui.ui +0 -0
  125. {finalcif-159 → finalcif-161}/finalcif/gui/text_value_editor.py +0 -0
  126. {finalcif-159 → finalcif-161}/finalcif/gui/validators.py +0 -0
  127. {finalcif-159 → finalcif-161}/finalcif/gui/vrf_classes.py +0 -0
  128. {finalcif-159 → finalcif-161}/finalcif/gui/vzs_viewer.py +0 -0
  129. {finalcif-159 → finalcif-161}/finalcif/icon/__init__.py +0 -0
  130. {finalcif-159 → finalcif-161}/finalcif/icon/finalcif.png +0 -0
  131. {finalcif-159 → finalcif-161}/finalcif/icon/finalcif2.ico +0 -0
  132. {finalcif-159 → finalcif-161}/finalcif/icon/finalcif2.png +0 -0
  133. {finalcif-159 → finalcif-161}/finalcif/icon/multitable.ico +0 -0
  134. {finalcif-159 → finalcif-161}/finalcif/icon/multitable.png +0 -0
  135. {finalcif-159 → finalcif-161}/finalcif/icon/multitable.xcf +0 -0
  136. {finalcif-159 → finalcif-161}/finalcif/report/__init__.py +0 -0
  137. {finalcif-159 → finalcif-161}/finalcif/report/archive_report.py +0 -0
  138. {finalcif-159 → finalcif-161}/finalcif/report/gui/__init__.py +0 -0
  139. {finalcif-159 → finalcif-161}/finalcif/report/gui/mainwindow.py +0 -0
  140. {finalcif-159 → finalcif-161}/finalcif/report/gui/mainwindow.ui +0 -0
  141. {finalcif-159 → finalcif-161}/finalcif/report/mtools.py +0 -0
  142. {finalcif-159 → finalcif-161}/finalcif/report/references.py +0 -0
  143. {finalcif-159 → finalcif-161}/finalcif/report/report_text.py +0 -0
  144. {finalcif-159 → finalcif-161}/finalcif/report/symm.py +0 -0
  145. {finalcif-159 → finalcif-161}/finalcif/report/tables.py +0 -0
  146. {finalcif-159 → finalcif-161}/finalcif/template/__init__.py +0 -0
  147. {finalcif-159 → finalcif-161}/finalcif/template/bootstrap/bootstrap.min.css +0 -0
  148. {finalcif-159 → finalcif-161}/finalcif/template/machines/D8_VENTURE.cif +0 -0
  149. {finalcif-159 → finalcif-161}/finalcif/template/mathml2omml.xsl +0 -0
  150. {finalcif-159 → finalcif-161}/finalcif/template/report_default.docx +0 -0
  151. {finalcif-159 → finalcif-161}/finalcif/template/template1.docx +0 -0
  152. {finalcif-159 → finalcif-161}/finalcif/template/template_for_multitable.docx +0 -0
  153. {finalcif-159/finalcif/tools → finalcif-161/finalcif/template/unicode2latex}/__init__.py +0 -0
  154. {finalcif-159 → finalcif-161}/finalcif/template/xsl/xmltohtml.xsl +0 -0
  155. {finalcif-159 → finalcif-161}/finalcif/tools/chemparse.py +0 -0
  156. {finalcif-159 → finalcif-161}/finalcif/tools/download.py +0 -0
  157. {finalcif-159 → finalcif-161}/finalcif/tools/dsrmath.py +0 -0
  158. {finalcif-159 → finalcif-161}/finalcif/tools/misc.py +0 -0
  159. {finalcif-159 → finalcif-161}/finalcif/tools/platon.py +0 -0
  160. {finalcif-159 → finalcif-161}/finalcif/tools/pupdate.py +0 -0
  161. {finalcif-159 → finalcif-161}/finalcif/tools/shred.py +0 -0
  162. {finalcif-159 → finalcif-161}/finalcif/tools/spgr_format.py +0 -0
  163. {finalcif-159 → finalcif-161}/finalcif/tools/statusbar.py +0 -0
  164. {finalcif-159 → finalcif-161}/finalcif/tools/sumformula.py +0 -0
  165. {finalcif-159 → finalcif-161}/finalcif.exe +0 -0
  166. {finalcif-159 → finalcif-161}/platon/README.txt +0 -0
  167. {finalcif-159 → finalcif-161}/platon/compile_platon.bat +0 -0
  168. {finalcif-159 → finalcif-161}/platon/platon_special.exe +0 -0
  169. {finalcif-159 → finalcif-161}/platon/platon_special.f +0 -0
  170. {finalcif-159 → finalcif-161}/pylock.toml +0 -0
  171. {finalcif-159 → finalcif-161}/run_finalcif +0 -0
  172. {finalcif-159 → finalcif-161}/run_finalcif.bat +0 -0
  173. {finalcif-159 → finalcif-161}/screenshots/finalcif_checkcif.png +0 -0
  174. {finalcif-159 → finalcif-161}/screenshots/finalcif_details.png +0 -0
  175. {finalcif-159 → finalcif-161}/screenshots/finalcif_loops.png +0 -0
  176. {finalcif-159 → finalcif-161}/screenshots/finalcif_loops2.png +0 -0
  177. {finalcif-159 → finalcif-161}/screenshots/finalcif_main.png +0 -0
  178. {finalcif-159 → finalcif-161}/screenshots/finalcif_report.png +0 -0
  179. {finalcif-159 → finalcif-161}/screenshots/finalcif_responses.png +0 -0
  180. {finalcif-159 → finalcif-161}/screenshots/loops_column_buttons.png +0 -0
  181. {finalcif-159 → finalcif-161}/update.exe +0 -0
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # pre-push hook: rejects pushing a version tag whose version
3
+ # does not match VERSION in finalcif/__init__.py.
4
+ #
5
+ # Activate with:
6
+ # git config core.hooksPath .githooks
7
+
8
+ INIT_FILE="finalcif/__init__.py"
9
+
10
+ # Extract VERSION from source (pure shell, no Python dependency)
11
+ get_source_version() {
12
+ grep -m1 '^VERSION' "$INIT_FILE" | sed 's/^VERSION[[:space:]]*=[[:space:]]*\([0-9]*\).*/\1/'
13
+ }
14
+
15
+ # Read stdin for refs being pushed
16
+ while read -r local_ref local_sha remote_ref remote_sha; do
17
+ # Only check version tags (refs/tags/*)
18
+ if [[ "$local_ref" == refs/tags/* ]]; then
19
+ tag_version="${local_ref#refs/tags/}"
20
+ tag_version="${tag_version#v}" # remove leading v if any
21
+
22
+ # Only process if tag is an integer
23
+ if [[ "$tag_version" =~ ^[0-9]+$ ]]; then
24
+ source_version=$(get_source_version)
25
+ if [ $? -ne 0 ] || [ -z "$source_version" ]; then
26
+ echo "ERROR: Could not extract VERSION from ${INIT_FILE}" >&2
27
+ exit 1
28
+ fi
29
+ if [ "$tag_version" != "$source_version" ]; then
30
+ echo "ERROR: Tag ${tag_version} does not match VERSION = ${source_version} in ${INIT_FILE}" >&2
31
+ echo "Update VERSION before tagging, or fix the tag name." >&2
32
+ exit 1
33
+ fi
34
+ fi
35
+ fi
36
+ done
37
+
38
+ exit 0
39
+
@@ -1,6 +1,6 @@
1
1
  <component name="ProjectRunConfigurationManager">
2
2
  <configuration default="false" name="compile_ui_files" type="PythonConfigurationType" factoryName="Python" activateToolWindowBeforeRun="false" nameIsGenerated="true">
3
- <module name="FinalCif" />
3
+ <module name="finalcif" />
4
4
  <option name="ENV_FILES" value="" />
5
5
  <option name="INTERPRETER_OPTIONS" value="" />
6
6
  <option name="PARENT_ENVS" value="true" />
finalcif-161/AGENTS.md ADDED
@@ -0,0 +1,58 @@
1
+ # FinalCif AI Developer Instructions
2
+
3
+ ## General Principles
4
+ - **No Assumptions:** If information or code is missing, ask for it. Do not make assumptions.
5
+ - **Refuse over Guessing:** Prefer refusing over guessing. If you don't know how to complete the code, say you don't know.
6
+ - **Require Specifications:** If the user asks you to write code, ask for a detailed specification first. Do not write code until you have a detailed specification.
7
+ - **Quality Standards:** Follow the rules of 'Refactoring' and 'Clean Code' as described by Martin Fowler and Robert C. Martin, respectively.
8
+
9
+ ## Core Setup Conventions
10
+ - **Language/Runtime:** Use Python >=3.12 (`pyproject.toml`), with CI tested on 3.12/3.13/3.14 (`.github/workflows/python_tests.yml`).
11
+ - **UI Framework:** Assume PySide6. UI imports in the codebase are typically routed through `qtpy` (note that UI files are compiled locally via bindings).
12
+ - **Qt Binding Selection:** Set `QT_API=pyside6` before `qtpy` imports (see `finalcif/finalcif_start.py`).
13
+
14
+ ## Architecture
15
+ - **Desktop Application:** FinalCif is a crystallographic information file (CIF) editor built with a Qt frontend.
16
+ - **Entry Point:** The application starts from `finalcif/finalcif_start.py` (registered as `finalcif` in `pyproject.toml`).
17
+ - **Main Window:** `AppWindow` (QMainWindow) in `finalcif/appwindow.py` is the orchestrator wiring all subsystems; UI defined in `finalcif/gui/finalcif_gui_ui.ui`.
18
+ - **Subpackage Map:**
19
+ - `finalcif/cif/` — CIF I/O via `gemmi.cif` wrapped in `CifContainer` (`cif_file_io.py`); dict definitions (core/powder/modulation/twin/restraints); subpackages `checkcif/` (IUCr CheckCIF HTML/PDF parsing) and `cod/` (COD/CCDC deposition: `deposit.py`, `upload.py`, `doi.py`, `website_parser.py`).
20
+ - `finalcif/ciforder/` — CIF key ordering dialog.
21
+ - `finalcif/datafiles/` — raw instrument data importers: Bruker (`bruker_data.py`, `bruker_frame.py`), SAINT, SADABS, SHELX `.lst` (`shelx_lst.py`), `.p4p`, CCDC deposition mail parser (`ccdc_mail.py`).
22
+ - `finalcif/displaymol/` — 3D molecular display (`vtk_molecule.py`) is not used; 2D rendering is provided by the external `fastmolwidget` package (custom widget in `finalcif_gui_ui.ui`).
23
+ - `finalcif/equip_property/` — Equipment/Properties/AuthorLoops template tables (`equipment.py`, `properties.py`, `author_loop_templates.py`).
24
+ - `finalcif/gui/` — Qt widgets; `.ui` files compiled to sibling `*_ui.py` modules.
25
+ - `finalcif/report/` — DOCX report generation via `docxtpl` (`DocxTemplate`, `RichText`, `InlineImage`) in `templated_report.py`; also archive reports, tables, references, symmetry.
26
+ - `finalcif/template/` — Report template infrastructure (`ReportTemplates`).
27
+ - `finalcif/tools/` — cross-cutting helpers: `platon.py` (`PlatonRunner`), `shred.py`, `space_groups.py`, `sumformula.py`, `settings.py` (`FinalCifSettings` QSettings wrapper), `download.py`, `pupdate.py` (auto-updater), `statusbar.py`, `chemparse.py`, `dsrmath.py`.
28
+ - **External Tool Integration:**
29
+ - Offline CheckCIF uses PLATON via `finalcif/tools/platon.py`; executable resolution checks bundled `platon/platon_special.exe`, then `C:\pwt\platon.exe` (Windows), then `platon` on `PATH`.
30
+ - Online CheckCIF (IUCr) via HTTP in `finalcif/cif/checkcif/checkcif.py`.
31
+ - CCDC/COD deposition in `finalcif/cif/cod/` (`deposit.py`, `upload.py`, `deposit_check.py`); deposition email parsing in `finalcif/datafiles/ccdc_mail.py`.
32
+ - DOI / CrossRef lookups via `crossrefapi` (see `finalcif/cif/cod/doi.py`).
33
+ - SHELX parsing via the external `shelxfile` package (used in `report/templated_report.py`); in-tree `.lst` parser in `datafiles/shelx_lst.py`.
34
+ - **CIF Parsing:** Standardized on `gemmi` (>=0.7.5); `gemmi.set_leak_warnings(False)` is set at startup.
35
+
36
+ ## Conventions
37
+ - **UI Files:** Edit `.ui` files in Qt Designer (`scripts/designer.py`), then regenerate via `python scripts/compile_ui_files.py`. Never hand-edit generated `*_ui.py`.
38
+ - **qtpy Imports:** `from qtpy import QtCore, QtGui, QtWebEngineWidgets, QtWidgets, compat` — `QtWebEngine` is used and requires `PySide6-Addons`.
39
+ - **App Bootstrap:** `appwindow.py` uses `QApplication.instance() or QApplication([])` with `app.setStyle("windowsvista")`. A `DEBUG` flag in `finalcif_start.py` toggles the crash hook; crash reports are written to `~/finalcif-crash.txt` by `my_exception_hook`.
40
+ - **Settings:** Persisted via `FinalCifSettings` (QSettings wrapper) in `finalcif/tools/settings.py`.
41
+
42
+ ## Workflow & Commands
43
+ - **Dependency Management:** The project uses `uv` for package management (`uv.lock`, `pylock.toml`, `pyproject.toml`).
44
+ - *Setup:* `uv sync --all-extras --dev`
45
+ - **Testing:**
46
+ - *Command:* `uv run pytest tests`
47
+ - *CI/Linux Headless:* run tests with `QT_QPA_PLATFORM=offscreen` (see `.github/workflows/python_tests.yml`).
48
+ - *Network-Gated Tests:* set `NO_NETWORK=1` to skip network-dependent checks (CheckCIF/DOI/deposit/PLATON; see `tests/test_checkcif.py`, `tests/test_doi.py`).
49
+ - *Helpers:* shared base class `AppWindowTestCase` and `processevents()` in `tests/helpers.py`; `get_platon_exe()` resolves `C:\pwt\platon.exe` on Windows or `which('platon')`. Fixtures live in `tests/examples/`, `tests/statics/`, `tests/checkcif_results/`, and top-level `test-data/`.
50
+ - **Linting:**
51
+ - *Command (local):* `ruff check .` — configured via `.ruff.toml` (line-length 120, target py312).
52
+ - *CI Lint:* GitHub Actions runs `flake8` in `.github/workflows/python_tests.yml` (ruff is not invoked in CI).
53
+ - **UI Compilation:**
54
+ - *Command:* `python scripts/compile_ui_files.py` (discovers `.ui` files, compiles them, and hot-fixes imports to `qtpy`).
55
+ - **Build/Releases:**
56
+ - Packaged with `hatchling`; tag pushes trigger `.github/workflows/python-dist.yml` which runs `uv build` and publishes to PyPI via trusted publishing.
57
+ - Desktop installers built with PyInstaller specs (`Finalcif_installer_win.spec`, `Finalcif_linux_onefile.spec`, `Finalcif_mac.spec`) plus `scripts/_make_win_release.py`, `scripts/make_win_release.bat`, and Inno Setup `scripts/finalcif-install_win64.iss`.
58
+ - Dev launchers: `run_finalcif.bat` / `run_finalcif` / `scripts/finalcif-start.sh`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finalcif
3
- Version: 159
3
+ Version: 161
4
4
  Summary: CIF file editor
5
5
  Project-URL: Homepage, https://dkratzert.de/finalcif.html
6
6
  Project-URL: Bug Tracker, https://github.com/dkratzert/FinalCif/issues
@@ -17,11 +17,11 @@ Requires-Dist: chardet
17
17
  Requires-Dist: charset-normalizer
18
18
  Requires-Dist: crossrefapi
19
19
  Requires-Dist: docxtpl[subdoc]>=0.20.2
20
- Requires-Dist: fastmolwidget
20
+ Requires-Dist: fastmolwidget>=0.9.5
21
21
  Requires-Dist: gemmi>=0.7.5
22
22
  Requires-Dist: hatchling
23
23
  Requires-Dist: html2text
24
- Requires-Dist: lxml==6.0.2
24
+ Requires-Dist: lxml==6.1.0
25
25
  Requires-Dist: numpy<2.4,>=2.3.5
26
26
  Requires-Dist: packaging
27
27
  Requires-Dist: pefile
@@ -75,6 +75,6 @@ You only need them if you want to make report tables that differ from the defaul
75
75
 
76
76
  ![FinalCif CheckCIF](https://github.com/dkratzert/FinalCif/raw/master/screenshots/finalcif_checkcif.png)
77
77
 
78
- ![FinalCif responses](https://github.com/dkratzert/FinalCif/raw/master/screenshots/finalcif_responses.png)
78
+ ![FinalCif responses](https://github.com/dkratzert/FinalCif/raw/master/docs/pics/finalcif_responses.png)
79
79
 
80
- ![FinalCif responses](https://github.com/dkratzert/FinalCif/raw/master/screenshots/finalcif_loops.png)
80
+ ![FinalCif loops](https://github.com/dkratzert/FinalCif/raw/master/screenshots/finalcif_loops.png)
@@ -35,6 +35,6 @@ You only need them if you want to make report tables that differ from the defaul
35
35
 
36
36
  ![FinalCif CheckCIF](https://github.com/dkratzert/FinalCif/raw/master/screenshots/finalcif_checkcif.png)
37
37
 
38
- ![FinalCif responses](https://github.com/dkratzert/FinalCif/raw/master/screenshots/finalcif_responses.png)
38
+ ![FinalCif responses](https://github.com/dkratzert/FinalCif/raw/master/docs/pics/finalcif_responses.png)
39
39
 
40
- ![FinalCif responses](https://github.com/dkratzert/FinalCif/raw/master/screenshots/finalcif_loops.png)
40
+ ![FinalCif loops](https://github.com/dkratzert/FinalCif/raw/master/screenshots/finalcif_loops.png)
@@ -0,0 +1,3 @@
1
+
2
+ VERSION = 161
3
+ __version__ = VERSION
@@ -18,6 +18,8 @@ from math import sin, radians
18
18
  from pathlib import Path, WindowsPath
19
19
  from typing import cast
20
20
 
21
+ from fastmolwidget import Atomtuple
22
+ from fastmolwidget.part_combo import PartFilterWidget
21
23
  from fastmolwidget.sdm import SDM
22
24
 
23
25
  from finalcif.gui.vzs_viewer import VZSImageViewer
@@ -113,6 +115,10 @@ class AppWindow(QMainWindow):
113
115
  self.ui = Ui_FinalCifWindow()
114
116
  self.ui.setupUi(self)
115
117
  self.ui.page_MainTable.setParent(self.ui.MainStackedWidget)
118
+ # Parts selector widget – inserted after adpCheckBox in horizontalLayout_7
119
+ self.parts_combo = PartFilterWidget(parent=self.ui.molGroupBox, label=' ', min_combo_width=90)
120
+ self.parts_combo.setObjectName('partsFilterWidget')
121
+ self.ui.horizontalLayout_7.insertWidget(3, self.parts_combo)
116
122
  # Inject the Author Editor tab (defined in the .ui file) into LoopsPage
117
123
  self.ui.loops_page.set_author_editor_tab(self.ui.author_editor_widget)
118
124
  self.fixfont = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.SystemFont.FixedFont)
@@ -204,7 +210,7 @@ class AppWindow(QMainWindow):
204
210
  self.ui.PictureWidthDoubleSpinBox.setRange(0.0, 25)
205
211
  self.ui.PictureWidthDoubleSpinBox.setSingleStep(0.5)
206
212
  # Just too slow for large structures:
207
- self.ui.growCheckBox.setChecked(False)
213
+ self.ui.growCheckBox.setChecked(True)
208
214
  self.ui.CheckcifButton.setDisabled(True)
209
215
  self.ui.CheckcifStartButton.setDisabled(True)
210
216
  self.ui.ReportPicPushButton.setDisabled(True)
@@ -419,6 +425,7 @@ class AppWindow(QMainWindow):
419
425
  self.ui.growCheckBox.toggled.connect(lambda: self.redraw_molecule(reset_view=False))
420
426
  self.ui.labelsCheckBox.toggled.connect(self.show_labels)
421
427
  self.ui.adpCheckBox.toggled.connect(self.show_adp)
428
+ self.parts_combo.selectionChanged.connect(self._on_parts_selection_changed)
422
429
  self.ui.SourcesPushButton.clicked.connect(self.show_sources)
423
430
  self.ui.BackSourcesPushButton.clicked.connect(self.back_to_main_noload)
424
431
  self.ui.BackFromOptionspPushButton.clicked.connect(self.back_to_main_noload)
@@ -1314,6 +1321,11 @@ class AppWindow(QMainWindow):
1314
1321
  ok = t.make_templated_html_report(output_filename=str(report_filename),
1315
1322
  template_path=template_path.parent,
1316
1323
  template_file=template_path.name)
1324
+ elif template_path.suffix in ('.tex', '.latex'):
1325
+ t = TemplatedReport(format=ReportFormat.LATEX, options=self.options, cif=self.cif)
1326
+ ok = t.make_templated_latex_report(output_filename=str(report_filename),
1327
+ template_path=template_path.parent,
1328
+ template_file=template_path.name)
1317
1329
  if not ok:
1318
1330
  return None
1319
1331
  except FileNotFoundError as e:
@@ -1965,14 +1977,15 @@ class AppWindow(QMainWindow):
1965
1977
  atoms = self.cif.atoms_orth
1966
1978
 
1967
1979
  with suppress(Exception):
1980
+ atom_tuples = self._atoms_with_adps(atoms)
1981
+ self._update_parts_combo(atom_tuples)
1968
1982
  if reset_view:
1969
- self.ui.render_widget.open_molecule(atoms,
1983
+ self.ui.render_widget.open_molecule(atom_tuples,
1970
1984
  cell=self.cif.cell[:6],
1971
- adps=self.get_adps(), keep_view=not reset_view)
1985
+ keep_view=not reset_view)
1972
1986
  else:
1973
- self.ui.render_widget.grow_molecule(atoms,
1974
- cell=self.cif.cell[:6],
1975
- adps=self.get_adps())
1987
+ self.ui.render_widget.grow_molecule(atom_tuples,
1988
+ cell=self.cif.cell[:6])
1976
1989
 
1977
1990
  def _calc_grown_atoms(self):
1978
1991
  """Helper to generate the symmetry expanded atoms without drawing them."""
@@ -1991,6 +2004,35 @@ class AppWindow(QMainWindow):
1991
2004
  to_float(dp.U23), to_float(dp.U13), to_float(dp.U12))
1992
2005
  return adp_dict
1993
2006
 
2007
+ def _atoms_with_adps(self, atoms) -> list[Atomtuple]:
2008
+ """Convert atom objects to Atomtuple instances with ADP data embedded directly."""
2009
+ adp_dict = self.get_adps()
2010
+ result = []
2011
+ for at in atoms:
2012
+ adp = adp_dict.get(at.label)
2013
+ if isinstance(at, Atomtuple):
2014
+ result.append(at._replace(adp=adp))
2015
+ else:
2016
+ result.append(Atomtuple(label=at.label, type=at.type,
2017
+ x=at.x, y=at.y, z=at.z,
2018
+ part=at.part, adp=adp))
2019
+ return result
2020
+
2021
+ def _update_parts_combo(self, atoms: list[Atomtuple]) -> None:
2022
+ """Repopulate the parts selector with the unique disorder parts present in *atoms*.
2023
+
2024
+ The widget is shown only when more than one distinct part is found (i.e. disorder exists).
2025
+ """
2026
+ parts = frozenset(at.part for at in atoms)
2027
+ self.parts_combo.update_parts(parts)
2028
+
2029
+ def _on_parts_selection_changed(self) -> None:
2030
+ """Forward the current parts selection to the molecule widget."""
2031
+ checked = self.parts_combo.checked_values()
2032
+ visible: set[int] | None = set(checked) if checked else None
2033
+ with suppress(Exception):
2034
+ self.ui.render_widget.set_visible_parts(visible)
2035
+
1994
2036
  def redraw_molecule(self, reset_view: bool = False) -> None:
1995
2037
  try:
1996
2038
  self.view_molecule(reset_view=reset_view)
@@ -12,6 +12,7 @@ from html import unescape
12
12
  import gemmi
13
13
 
14
14
 
15
+
15
16
  def quote(string: str, wrapping=80) -> str:
16
17
  """
17
18
  Quotes a cif string and wraps it. The shorter strings are directly handled by cif.quote().
@@ -28,56 +29,56 @@ def quote(string: str, wrapping=80) -> str:
28
29
 
29
30
 
30
31
  characters = {
31
- '—' : r'--',
32
- '±' : r'+-',
33
- '×' : r'\\times', # noqa: RUF001
34
- '≠' : r'\\neq',
35
- '→' : r'\\rightarrow',
36
- '←' : r'\\leftarrow',
37
- '∞' : r'\\infty',
38
- '≃' : r'\\simeq',
39
- '≈' : r'\\sim',
40
- 'ß' : r'\&s',
41
- 'ü' : r'\"u',
42
- 'Ü' : r'\"U',
43
- 'ö' : r'\"o', # 'LATIN SMALL LETTER O WITH DIAERESIS'
44
- 'Ö' : r'\"O',
45
- 'ä' : r'\"a',
46
- 'Ä' : r'\"A',
47
- 'é' : r"\'e",
48
- 'É' : r"\'E",
49
- 'è' : r'\`e',
50
- 'È' : r'\`E',
51
- 'ì' : r'\`i',
52
- 'Ì' : r'\`I',
53
- 'í' : r'\'i',
54
- 'Í' : r'\'I',
55
- 'ó' : r"\'o",
56
- 'Ó' : r"\'O",
57
- 'ò' : r'\`o',
58
- 'Ò' : r'\`O',
59
- 'ȯ' : r'\.o',
60
- 'ø' : r'\/o',
61
- 'á' : r'\'a',
62
- 'Á' : r'\'A',
63
- 'à' : r'\`a',
64
- 'À' : r'\`A',
65
- 'â' : r'\^a',
66
- 'Â' : r'\^A',
67
- 'å' : r'\%a',
68
- 'ê' : r'\^e',
69
- 'î' : r'\^i',
70
- 'Î' : r'\^I',
71
- 'ô' : r'\^o',
72
- 'Ô' : r'\^O',
73
- 'û' : r'\^u',
74
- 'Û' : r'\^U',
75
- 'ç' : r'\,c',
76
- 'ñ' : r'\~n',
77
- 'ł' : r'\/l',
78
- 'đ' : r'\/d',
79
- 'Å' : r'\%A', # 'LATIN CAPITAL LETTER A WITH RING ABOVE'
80
- 'Å' : r'\%A', # unicodedata.name('Å'): 'ANGSTROM SIGN'
32
+ '—' : r'--',
33
+ '±' : r'+-',
34
+ '×' : r'\\times', # noqa: RUF001
35
+ '≠' : r'\\neq',
36
+ '→' : r'\\rightarrow',
37
+ '←' : r'\\leftarrow',
38
+ '∞' : r'\\infty',
39
+ '≃' : r'\\simeq',
40
+ '≈' : r'\\sim',
41
+ 'ß' : r'\&s',
42
+ 'ü' : r'\"u',
43
+ 'Ü' : r'\"U',
44
+ 'ö' : r'\"o', # 'LATIN SMALL LETTER O WITH DIAERESIS'
45
+ 'Ö' : r'\"O',
46
+ 'ä' : r'\"a',
47
+ 'Ä' : r'\"A',
48
+ 'é' : r"\'e",
49
+ 'É' : r"\'E",
50
+ 'è' : r'\`e',
51
+ 'È' : r'\`E',
52
+ 'ì' : r'\`i',
53
+ 'Ì' : r'\`I',
54
+ 'í' : r'\'i',
55
+ 'Í' : r'\'I',
56
+ 'ó' : r"\'o",
57
+ 'Ó' : r"\'O",
58
+ 'ò' : r'\`o',
59
+ 'Ò' : r'\`O',
60
+ 'ȯ' : r'\.o',
61
+ 'ø' : r'\/o',
62
+ 'á' : r'\'a',
63
+ 'Á' : r'\'A',
64
+ 'à' : r'\`a',
65
+ 'À' : r'\`A',
66
+ 'â' : r'\^a',
67
+ 'Â' : r'\^A',
68
+ 'å' : r'\%a',
69
+ 'ê' : r'\^e',
70
+ 'î' : r'\^i',
71
+ 'Î' : r'\^I',
72
+ 'ô' : r'\^o',
73
+ 'Ô' : r'\^O',
74
+ 'û' : r'\^u',
75
+ 'Û' : r'\^U',
76
+ 'ç' : r'\,c',
77
+ 'ñ' : r'\~n',
78
+ 'ł' : r'\/l',
79
+ 'đ' : r'\/d',
80
+ 'Å' : r'\%A', # 'LATIN CAPITAL LETTER A WITH RING ABOVE'
81
+ 'Å' : r'\%A', # unicodedata.name('Å'): 'ANGSTROM SIGN'
81
82
  '\u2079': r'^9^',
82
83
  '\u2078': r'^8^',
83
84
  '\u2077': r'^7^',
@@ -149,7 +150,7 @@ characters = {
149
150
  "\u03A9": r'\W',
150
151
  "\u03D5": r'\f',
151
152
  "\u00B0": r"\%",
152
- '·' : r"{middle dot}",
153
+ '·' : r"{middle dot}",
153
154
  # "1̄": r'\=1', # Does not work in QT?
154
155
  }
155
156
 
@@ -199,6 +200,17 @@ def string_to_utf8(txt: str) -> str:
199
200
  return retranslate_delimiter(txt)
200
201
 
201
202
 
203
+ def string_to_latex(cif_string: str) -> str:
204
+ from finalcif.template.unicode2latex.u2l import uni2tex
205
+ utf8_string = string_to_utf8(cif_string)
206
+ return uni2tex(utf8_string, add_font_modifiers=False, prefer_unicode_math=False, convert_accents=True)
207
+
208
+
209
+ def utf8_to_latex(utf8_string: str) -> str:
210
+ from finalcif.template.unicode2latex.u2l import uni2tex
211
+ return uni2tex(utf8_string, add_font_modifiers=False, prefer_unicode_math=False, convert_accents=True)
212
+
213
+
202
214
  def retranslate_delimiter(txt: str, no_html_unescape: bool = False) -> str:
203
215
  """
204
216
  Translates delimited cif characters back to Unicode characters.
@@ -219,3 +231,26 @@ def utf8_to_html_ascii(text: str) -> str:
219
231
 
220
232
  def html_ascii_to_utf8(text: str) -> str:
221
233
  return unescape(text)
234
+
235
+
236
+ def escape_for_latex(text: str) -> str:
237
+ """
238
+ Escapes special characters in text so that it can be safely used in LaTeX.
239
+ """
240
+ latex_special_chars = {
241
+ '\\': r'\textbackslash{}',
242
+ '{' : r'\{',
243
+ '}' : r'\}',
244
+ '$' : r'\$',
245
+ '&' : r'\&',
246
+ '#' : r'\#',
247
+ '_' : r'\_',
248
+ '%' : r'\%',
249
+ '~' : r'\textasciitilde{}',
250
+ '^' : r'\textasciicircum{}',
251
+ '[' : r'\[',
252
+ ']' : r'\]',
253
+ }
254
+ regex = re.compile('|'.join(re.escape(key) for key in latex_special_chars))
255
+ escaped_text = regex.sub(lambda match: latex_special_chars[match.group()], text)
256
+ return string_to_latex(escaped_text)
@@ -13,7 +13,8 @@ from pathlib import Path
13
13
  from typing import TYPE_CHECKING
14
14
 
15
15
  import gemmi.cif
16
- from qtpy.QtWidgets import QListWidgetItem
16
+ from qtpy import QtCore
17
+ from qtpy.QtWidgets import QListWidgetItem, QMenu
17
18
  from gemmi.cif import Loop, as_string
18
19
 
19
20
  from finalcif.cif.cif_file_io import CifContainer
@@ -54,6 +55,8 @@ class AuthorLoops:
54
55
  self.ui = ui
55
56
  self.cif = cif
56
57
  self.app = app
58
+ self._rename_old_name: str = ''
59
+ self._rename_menu: QMenu | None = None
57
60
  self.cif_key = '_publ_author'
58
61
  self.keys_list = [f'{self.cif_key}_name', f'{self.cif_key}_address', f'{self.cif_key}_email',
59
62
  f'{self.cif_key}_phone', f'{self.cif_key}_id_orcid', f'{self.cif_key}_id_iucr',
@@ -86,6 +89,11 @@ class AuthorLoops:
86
89
  self.ui.ImportAuthorPushButton.clicked.connect(self.import_author)
87
90
  self.ui.FullNameLineEdit.textChanged.connect(self.make_sure_to_save_only_when_name_field_has_text)
88
91
  self.ui.FullNameLineEdit_cif.textChanged.connect(self.make_sure_to_save_only_when_name_field_has_text)
92
+ self.ui.LoopTemplatesListWidget.setContextMenuPolicy(
93
+ QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
94
+ self.ui.LoopTemplatesListWidget.customContextMenuRequested.connect(
95
+ self.show_author_rename_menu)
96
+ self.ui.LoopTemplatesListWidget.itemChanged.connect(self.on_author_item_renamed)
89
97
 
90
98
  def set_author_state(self) -> None:
91
99
  if self.ui.authorEditTabWidget.currentWidget().objectName() == 'page_publication':
@@ -242,16 +250,30 @@ class AuthorLoops:
242
250
  self.general_author_save(author)
243
251
  self.clear_fields()
244
252
 
245
- def general_author_save(self, author: Author) -> None:
253
+ def _author_template_name(self, author: Author) -> str:
254
+ """Return the default template-list name for an author."""
255
+ if author.contact_author:
256
+ return f'{string_to_utf8(as_string(author.name))} (contact author)'
257
+ return string_to_utf8(as_string(author.name))
258
+
259
+ def _unique_author_name(self, base_name: str) -> str:
260
+ """Return a name not yet in the authors list, appending a counter if necessary."""
261
+ existing = set(self.authors_list())
262
+ if base_name not in existing:
263
+ return base_name
264
+ counter = 2
265
+ while f'{base_name} {counter}' in existing:
266
+ counter += 1
267
+ return f'{base_name} {counter}'
268
+
269
+ def general_author_save(self, author: Author, template_name: str | None = None) -> None:
246
270
  if not author.name:
247
271
  return
248
- if author.contact_author:
249
- itemtext = f'{string_to_utf8(as_string(author.name))} (contact author)'
250
- else:
251
- itemtext = string_to_utf8(as_string(author.name))
252
- if not itemtext:
272
+ if template_name is None:
273
+ template_name = self._author_template_name(author)
274
+ if not template_name:
253
275
  return
254
- self.settings.save_settings_dict(property='authors_list', name=itemtext, items=author)
276
+ self.settings.save_settings_dict(property='authors_list', name=template_name, items=author)
255
277
  self.show_authors_list()
256
278
 
257
279
  def export_author_template(self, filename: str | None = None) -> None:
@@ -341,7 +363,8 @@ class AuthorLoops:
341
363
  author_type=AuthorType.publ,
342
364
  contact_author=True
343
365
  )
344
- self.general_author_save(author)
366
+ unique_name = self._unique_author_name(self._author_template_name(author))
367
+ self.general_author_save(author, template_name=unique_name)
345
368
 
346
369
  names = list(block.find_values('_publ_author_name'))
347
370
  if names:
@@ -368,7 +391,8 @@ class AuthorLoops:
368
391
  author_type=AuthorType.publ,
369
392
  contact_author=False
370
393
  )
371
- self.general_author_save(author)
394
+ unique_name = self._unique_author_name(self._author_template_name(author))
395
+ self.general_author_save(author, template_name=unique_name)
372
396
 
373
397
  self.show_authors_list()
374
398
 
@@ -401,6 +425,58 @@ class AuthorLoops:
401
425
  if author:
402
426
  self.ui.LoopTemplatesListWidget.addItem(QListWidgetItem(author))
403
427
 
428
+ def show_author_rename_menu(self, pos: QtCore.QPoint) -> None:
429
+ """Show a context menu with a Rename action for the author loop templates list."""
430
+ listw = self.ui.LoopTemplatesListWidget
431
+ item = listw.itemAt(pos)
432
+ if item is None:
433
+ return
434
+ menu = QMenu(listw)
435
+ rename_action = menu.addAction('Rename')
436
+ # Keep a strong Python reference so PySide6 does not garbage-collect the
437
+ # menu wrapper (and thereby drop the triggered signal connection) before
438
+ # the user selects an action. Clear it whenever the menu closes so the
439
+ # reference is not held indefinitely if the user dismisses without action.
440
+ self._rename_menu = menu
441
+ menu.aboutToHide.connect(lambda: setattr(self, '_rename_menu', None))
442
+
443
+ def _on_triggered(action) -> None:
444
+ if action == rename_action:
445
+ # Set flags first so the spurious itemChanged signal (fired by
446
+ # setFlags) finds _rename_old_name still empty and returns early.
447
+ item.setFlags(
448
+ QtCore.Qt.ItemFlag.ItemIsEditable
449
+ | QtCore.Qt.ItemFlag.ItemIsEnabled
450
+ | QtCore.Qt.ItemFlag.ItemIsSelectable
451
+ )
452
+ self._rename_old_name = item.text()
453
+ # Defer editItem() to the next event-loop tick so that all
454
+ # menu-close events are processed before Qt starts the editor.
455
+ QtCore.QTimer.singleShot(0, lambda: listw.editItem(item))
456
+
457
+ menu.triggered.connect(_on_triggered)
458
+ menu.popup(listw.viewport().mapToGlobal(pos))
459
+
460
+ def on_author_item_renamed(self, item: QListWidgetItem) -> None:
461
+ """Persist an author template rename when the user finishes editing."""
462
+ if not self._rename_old_name:
463
+ return
464
+ new_name = item.text().strip()
465
+ old_name = self._rename_old_name
466
+ self._rename_old_name = ''
467
+ # Remove the editable flag so items cannot be renamed by accident
468
+ item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable)
469
+ if not new_name:
470
+ item.setText(old_name)
471
+ return
472
+ if new_name != old_name:
473
+ if new_name in self.authors_list():
474
+ show_general_warning(self.app, f'A template named "{new_name}" already exists.')
475
+ item.setText(old_name)
476
+ return
477
+ self.settings.rename_template('authors_list', old_name, new_name)
478
+ self.show_authors_list()
479
+
404
480
  def export_raw_data(self) -> list[Author]:
405
481
  """
406
482
  Export all authors in order to export them all"""
@@ -415,7 +491,8 @@ class AuthorLoops:
415
491
  Import all authors from an external file"""
416
492
  for author in authors_data:
417
493
  if author and isinstance(author, Author):
418
- self.general_author_save(author)
494
+ unique_name = self._unique_author_name(self._author_template_name(author))
495
+ self.general_author_save(author, template_name=unique_name)
419
496
 
420
497
  def authors_list(self) -> list[str]:
421
498
  return self.settings.list_saved_items(property='authors_list')