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.
- finalcif-161/.githooks/pre-push +39 -0
- {finalcif-159 → finalcif-161}/.run/compile_ui_files.run.xml +1 -1
- finalcif-161/AGENTS.md +58 -0
- {finalcif-159 → finalcif-161}/PKG-INFO +5 -5
- {finalcif-159 → finalcif-161}/README.md +2 -2
- finalcif-161/finalcif/__init__.py +3 -0
- {finalcif-159 → finalcif-161}/finalcif/appwindow.py +48 -6
- {finalcif-159 → finalcif-161}/finalcif/cif/text.py +86 -51
- {finalcif-159 → finalcif-161}/finalcif/equip_property/author_loop_templates.py +88 -11
- {finalcif-159 → finalcif-161}/finalcif/equip_property/equipment.py +73 -1
- {finalcif-159 → finalcif-161}/finalcif/equip_property/properties.py +60 -1
- {finalcif-159 → finalcif-161}/finalcif/report/templated_report.py +109 -12
- finalcif-161/finalcif/template/report.tex +240 -0
- {finalcif-159 → finalcif-161}/finalcif/template/report.tmpl +4 -3
- finalcif-161/finalcif/template/report2.tex +249 -0
- finalcif-161/finalcif/template/template_text.docx +0 -0
- finalcif-161/finalcif/template/template_without_text.docx +0 -0
- {finalcif-159 → finalcif-161}/finalcif/template/templates.py +4 -3
- finalcif-161/finalcif/template/unicode2latex/FakePlasTeX/FakeContext.py +77 -0
- finalcif-161/finalcif/template/unicode2latex/FakePlasTeX/FakeTokenizer.py +493 -0
- finalcif-161/finalcif/template/unicode2latex/FakePlasTeX/README.md +4 -0
- finalcif-161/finalcif/template/unicode2latex/FakePlasTeX/TokenizerPassThru.py +190 -0
- finalcif-161/finalcif/template/unicode2latex/LICENSE +25 -0
- finalcif-161/finalcif/template/unicode2latex/u2l.py +647 -0
- finalcif-161/finalcif/template/unicode2latex/unicode-math-table.tex +2469 -0
- finalcif-161/finalcif/template/unicode2latex/unicode-math-xetex.sty +3882 -0
- {finalcif-159 → finalcif-161}/finalcif/template/xsl/convert.py +24 -2
- finalcif-161/finalcif/template/xsl/xmltolatex.xsl +85 -0
- finalcif-161/finalcif/tools/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/options.py +12 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/settings.py +27 -1
- {finalcif-159 → finalcif-161}/finalcif/tools/space_groups.py +21 -0
- {finalcif-159 → finalcif-161}/pyproject.toml +3 -3
- {finalcif-159 → finalcif-161}/uv.lock +101 -85
- finalcif-159/AGENTS.md +0 -31
- finalcif-159/finalcif/__init__.py +0 -2
- finalcif-159/finalcif/template/template_text.docx +0 -0
- finalcif-159/finalcif/template/template_without_text.docx +0 -0
- {finalcif-159 → finalcif-161}/.gitattributes +0 -0
- {finalcif-159 → finalcif-161}/.gitignore +0 -0
- {finalcif-159 → finalcif-161}/.python-version +0 -0
- {finalcif-159 → finalcif-161}/.ruff.toml +0 -0
- {finalcif-159 → finalcif-161}/.run/Make Documentation MAC.run.xml +0 -0
- {finalcif-159 → finalcif-161}/.run/make documentation Windows.run.xml +0 -0
- {finalcif-159 → finalcif-161}/CODE_OF_CONDUCT.md +0 -0
- {finalcif-159 → finalcif-161}/CONTRIBUTING.md +0 -0
- {finalcif-159 → finalcif-161}/Finalcif_installer_win.spec +0 -0
- {finalcif-159 → finalcif-161}/Finalcif_linux_onefile.spec +0 -0
- {finalcif-159 → finalcif-161}/Finalcif_mac.spec +0 -0
- {finalcif-159 → finalcif-161}/LICENSE +0 -0
- {finalcif-159 → finalcif-161}/finalcif/app_path.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/all_cif_dicts.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/atoms.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/checkcif/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/checkcif/checkcif.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cif_dict_foo.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cif_file_io.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cif_order.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cod/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cod/deposit.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cod/deposit_check.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cod/deposition_list.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cod/doi.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cod/upload.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/cod/website_parser.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/core_dict.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/hkl.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/modulation_dict.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/powder_dict.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/reference.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/restraints_dict.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/twin_dict.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/cif/vrf_entry.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/ciforder/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/ciforder/order.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/ciforder/order_ui.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/ciforder/order_ui.ui +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/bruker_data.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/bruker_frame.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/ccdc_mail.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/data.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/p4p_reader.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/sadabs.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/saint.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/shelx_lst.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/datafiles/utils.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/displaymol/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/displaymol/vtk_molecule.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/equip_property/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/equip_property/tools.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/finalcif_start.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/block_combobox.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/cif_table_model.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/cif_table_view.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/combobox.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/custom_classes.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/dialogs.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/edit_button.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/equipmenttable.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/file_editor.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/finalcif_gui_ui.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/finalcif_gui_ui.ui +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/import_selector.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/import_selector_ui.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/import_selector_ui.ui +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/loop_creator.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/loop_creator_ui.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/loop_creator_ui.ui +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/loops.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/mainstackwidget.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/mixins.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/new_key_dialog.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/new_key_dialog_ui.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/new_key_dialog_ui.ui +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/plaintextedit.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/playground.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/propertytable.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/spell_check_edit.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/syntax_highlighter.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/text_templates_ui.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/text_templates_ui.ui +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/text_value_editor.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/validators.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/vrf_classes.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/gui/vzs_viewer.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/icon/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/icon/finalcif.png +0 -0
- {finalcif-159 → finalcif-161}/finalcif/icon/finalcif2.ico +0 -0
- {finalcif-159 → finalcif-161}/finalcif/icon/finalcif2.png +0 -0
- {finalcif-159 → finalcif-161}/finalcif/icon/multitable.ico +0 -0
- {finalcif-159 → finalcif-161}/finalcif/icon/multitable.png +0 -0
- {finalcif-159 → finalcif-161}/finalcif/icon/multitable.xcf +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/archive_report.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/gui/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/gui/mainwindow.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/gui/mainwindow.ui +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/mtools.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/references.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/report_text.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/symm.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/report/tables.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/template/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/template/bootstrap/bootstrap.min.css +0 -0
- {finalcif-159 → finalcif-161}/finalcif/template/machines/D8_VENTURE.cif +0 -0
- {finalcif-159 → finalcif-161}/finalcif/template/mathml2omml.xsl +0 -0
- {finalcif-159 → finalcif-161}/finalcif/template/report_default.docx +0 -0
- {finalcif-159 → finalcif-161}/finalcif/template/template1.docx +0 -0
- {finalcif-159 → finalcif-161}/finalcif/template/template_for_multitable.docx +0 -0
- {finalcif-159/finalcif/tools → finalcif-161/finalcif/template/unicode2latex}/__init__.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/template/xsl/xmltohtml.xsl +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/chemparse.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/download.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/dsrmath.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/misc.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/platon.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/pupdate.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/shred.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/spgr_format.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/statusbar.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif/tools/sumformula.py +0 -0
- {finalcif-159 → finalcif-161}/finalcif.exe +0 -0
- {finalcif-159 → finalcif-161}/platon/README.txt +0 -0
- {finalcif-159 → finalcif-161}/platon/compile_platon.bat +0 -0
- {finalcif-159 → finalcif-161}/platon/platon_special.exe +0 -0
- {finalcif-159 → finalcif-161}/platon/platon_special.f +0 -0
- {finalcif-159 → finalcif-161}/pylock.toml +0 -0
- {finalcif-159 → finalcif-161}/run_finalcif +0 -0
- {finalcif-159 → finalcif-161}/run_finalcif.bat +0 -0
- {finalcif-159 → finalcif-161}/screenshots/finalcif_checkcif.png +0 -0
- {finalcif-159 → finalcif-161}/screenshots/finalcif_details.png +0 -0
- {finalcif-159 → finalcif-161}/screenshots/finalcif_loops.png +0 -0
- {finalcif-159 → finalcif-161}/screenshots/finalcif_loops2.png +0 -0
- {finalcif-159 → finalcif-161}/screenshots/finalcif_main.png +0 -0
- {finalcif-159 → finalcif-161}/screenshots/finalcif_report.png +0 -0
- {finalcif-159 → finalcif-161}/screenshots/finalcif_responses.png +0 -0
- {finalcif-159 → finalcif-161}/screenshots/loops_column_buttons.png +0 -0
- {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="
|
|
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:
|
|
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
|
|
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
|

|
|
77
77
|
|
|
78
|
-

|
|
79
79
|
|
|
80
|
-

|
|
@@ -35,6 +35,6 @@ You only need them if you want to make report tables that differ from the defaul
|
|
|
35
35
|
|
|
36
36
|

|
|
37
37
|
|
|
38
|
-

|
|
39
39
|
|
|
40
|
-

|
|
@@ -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(
|
|
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(
|
|
1983
|
+
self.ui.render_widget.open_molecule(atom_tuples,
|
|
1970
1984
|
cell=self.cif.cell[:6],
|
|
1971
|
-
|
|
1985
|
+
keep_view=not reset_view)
|
|
1972
1986
|
else:
|
|
1973
|
-
self.ui.render_widget.grow_molecule(
|
|
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
|
-
'—'
|
|
32
|
-
'±'
|
|
33
|
-
'×'
|
|
34
|
-
'≠'
|
|
35
|
-
'→'
|
|
36
|
-
'←'
|
|
37
|
-
'∞'
|
|
38
|
-
'≃'
|
|
39
|
-
'≈'
|
|
40
|
-
'ß'
|
|
41
|
-
'ü'
|
|
42
|
-
'Ü'
|
|
43
|
-
'ö'
|
|
44
|
-
'Ö'
|
|
45
|
-
'ä'
|
|
46
|
-
'Ä'
|
|
47
|
-
'é'
|
|
48
|
-
'É'
|
|
49
|
-
'è'
|
|
50
|
-
'È'
|
|
51
|
-
'ì'
|
|
52
|
-
'Ì'
|
|
53
|
-
'í'
|
|
54
|
-
'Í'
|
|
55
|
-
'ó'
|
|
56
|
-
'Ó'
|
|
57
|
-
'ò'
|
|
58
|
-
'Ò'
|
|
59
|
-
'ȯ'
|
|
60
|
-
'ø'
|
|
61
|
-
'á'
|
|
62
|
-
'Á'
|
|
63
|
-
'à'
|
|
64
|
-
'À'
|
|
65
|
-
'â'
|
|
66
|
-
'Â'
|
|
67
|
-
'å'
|
|
68
|
-
'ê'
|
|
69
|
-
'î'
|
|
70
|
-
'Î'
|
|
71
|
-
'ô'
|
|
72
|
-
'Ô'
|
|
73
|
-
'û'
|
|
74
|
-
'Û'
|
|
75
|
-
'ç'
|
|
76
|
-
'ñ'
|
|
77
|
-
'ł'
|
|
78
|
-
'đ'
|
|
79
|
-
'Å'
|
|
80
|
-
'Å'
|
|
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
|
-
'·'
|
|
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
|
|
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
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
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=
|
|
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.
|
|
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.
|
|
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.
|
|
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')
|