MoleditPy-linux 4.0.2__tar.gz → 4.1.1__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 (80) hide show
  1. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/PKG-INFO +5 -5
  2. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/README.md +4 -4
  3. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/pyproject.toml +1 -1
  4. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/MoleditPy_linux.egg-info/PKG-INFO +5 -5
  5. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/core/mol_geometry.py +24 -8
  6. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/main.py +41 -6
  7. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/plugins/plugin_interface.py +36 -33
  8. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/plugins/plugin_manager_window.py +1 -1
  9. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/app_state.py +3 -7
  10. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/atom_item.py +50 -19
  11. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/bond_item.py +27 -23
  12. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/calculation_worker.py +80 -38
  13. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/compute_logic.py +6 -6
  14. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +1 -4
  15. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/edit_actions_logic.py +12 -5
  16. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/export_logic.py +2 -1
  17. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/io_logic.py +5 -5
  18. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/molecule_scene.py +22 -15
  19. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/periodic_table_dialog.py +8 -6
  20. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +47 -0
  21. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +33 -0
  22. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/user_template_dialog.py +1 -1
  23. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/view_3d_logic.py +0 -2
  24. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/zoomable_view.py +4 -0
  25. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/utils/default_settings.py +5 -0
  26. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/LICENSE +0 -0
  27. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/setup.cfg +0 -0
  28. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  29. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  30. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  31. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  32. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  33. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/__init__.py +0 -0
  34. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/__main__.py +0 -0
  35. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  36. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/assets/icon.icns +0 -0
  37. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/assets/icon.ico +0 -0
  38. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/assets/icon.png +0 -0
  39. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/core/__init__.py +0 -0
  40. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/core/molecular_data.py +0 -0
  41. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/plugins/__init__.py +0 -0
  42. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
  43. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/__init__.py +0 -0
  44. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/about_dialog.py +0 -0
  45. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
  46. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
  47. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  48. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
  49. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/atom_picking.py +0 -0
  50. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  51. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
  52. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  53. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
  54. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  55. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
  56. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
  57. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
  58. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
  59. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  60. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/main_window.py +0 -0
  61. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/main_window_init.py +0 -0
  62. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
  63. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
  64. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
  65. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
  66. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
  67. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
  68. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  69. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  70. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
  71. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
  72. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/string_importers.py +0 -0
  73. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  74. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
  75. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
  76. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/ui/ui_manager.py +0 -0
  77. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/utils/__init__.py +0 -0
  78. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/utils/constants.py +0 -0
  79. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  80. {moleditpy_linux-4.0.2 → moleditpy_linux-4.1.1}/src/moleditpy_linux/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.0.2
3
+ Version: 4.1.1
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -837,9 +837,9 @@ moleditpy
837
837
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
838
838
  * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
839
839
 
840
- ## License
840
+ ## License & Disclaimer
841
841
 
842
- This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**. See the `LICENSE` file for details.
842
+ This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the [LICENSE](LICENSE) file for details. As open-source software, it is provided 'as is' without warranty of any kind, and the author assumes no responsibility or liability for the results. Although outputs have been carefully verified, users are strongly encouraged to independently check and validate them for critical applications (such as publications). If you encounter any bugs, please open an issue.
843
843
 
844
844
  ## Citation
845
845
 
@@ -971,9 +971,9 @@ moleditpy
971
971
  * **化学計算 (RDKit / Open Babel):** 2DデータからRDKit分子オブジェクトを生成し、3D座標生成や分子特性計算を実行します。RDKitでの3D座標生成が失敗した際は、Open Babelにフォールバックします。重い計算処理は別スレッド (`QThread`) で実行し、GUIの応答性を維持しています。
972
972
  * **3D可視化 (PyVista / pyvistaqt):** RDKitのコンフォーマ座標からPyVistaのメッシュ(球や円柱)を生成して描画します。カスタムの`vtkInteractorStyle`を実装し、3Dビュー内での原子の直接的なドラッグ&ドロップ編集を可能にしています。
973
973
 
974
- ## ライセンス
974
+ ## ライセンス & 免責事項
975
975
 
976
- このプロジェクトは **GNU General Public License v3.0 (GPL-v3)** のもとで公開されています。詳細は `LICENSE` ファイルを参照してください。
976
+ このプロジェクトは GNU General Public License v3.0 (GPLv3) のもとでライセンスされています。詳細は [LICENSE](LICENSE) ファイルを参照してください。オープンソースソフトウェアとして、本ソフトウェアは「現状のまま」提供され、いかなる明示または黙示の保証も行いません。また、本ソフトウェアを使用した結果について、作者は一切の責任や義務を負いません。出力結果は慎重に検証されていますが、学術論文の作成など重要な用途においては、ユーザーご自身で結果を独立して確認および検証することを強くお勧めします。バグに遭遇した場合は、Issueを作成してください。
977
977
 
978
978
  ## 引用
979
979
 
@@ -134,9 +134,9 @@ moleditpy
134
134
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
135
135
  * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
136
136
 
137
- ## License
137
+ ## License & Disclaimer
138
138
 
139
- This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**. See the `LICENSE` file for details.
139
+ This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the [LICENSE](LICENSE) file for details. As open-source software, it is provided 'as is' without warranty of any kind, and the author assumes no responsibility or liability for the results. Although outputs have been carefully verified, users are strongly encouraged to independently check and validate them for critical applications (such as publications). If you encounter any bugs, please open an issue.
140
140
 
141
141
  ## Citation
142
142
 
@@ -268,9 +268,9 @@ moleditpy
268
268
  * **化学計算 (RDKit / Open Babel):** 2DデータからRDKit分子オブジェクトを生成し、3D座標生成や分子特性計算を実行します。RDKitでの3D座標生成が失敗した際は、Open Babelにフォールバックします。重い計算処理は別スレッド (`QThread`) で実行し、GUIの応答性を維持しています。
269
269
  * **3D可視化 (PyVista / pyvistaqt):** RDKitのコンフォーマ座標からPyVistaのメッシュ(球や円柱)を生成して描画します。カスタムの`vtkInteractorStyle`を実装し、3Dビュー内での原子の直接的なドラッグ&ドロップ編集を可能にしています。
270
270
 
271
- ## ライセンス
271
+ ## ライセンス & 免責事項
272
272
 
273
- このプロジェクトは **GNU General Public License v3.0 (GPL-v3)** のもとで公開されています。詳細は `LICENSE` ファイルを参照してください。
273
+ このプロジェクトは GNU General Public License v3.0 (GPLv3) のもとでライセンスされています。詳細は [LICENSE](LICENSE) ファイルを参照してください。オープンソースソフトウェアとして、本ソフトウェアは「現状のまま」提供され、いかなる明示または黙示の保証も行いません。また、本ソフトウェアを使用した結果について、作者は一切の責任や義務を負いません。出力結果は慎重に検証されていますが、学術論文の作成など重要な用途においては、ユーザーご自身で結果を独立して確認および検証することを強くお勧めします。バグに遭遇した場合は、Issueを作成してください。
274
274
 
275
275
  ## 引用
276
276
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "4.0.2"
8
+ version = "4.1.1"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.0.2
3
+ Version: 4.1.1
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -837,9 +837,9 @@ moleditpy
837
837
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
838
838
  * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
839
839
 
840
- ## License
840
+ ## License & Disclaimer
841
841
 
842
- This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**. See the `LICENSE` file for details.
842
+ This project is licensed under the GNU General Public License v3.0 (GPLv3) - see the [LICENSE](LICENSE) file for details. As open-source software, it is provided 'as is' without warranty of any kind, and the author assumes no responsibility or liability for the results. Although outputs have been carefully verified, users are strongly encouraged to independently check and validate them for critical applications (such as publications). If you encounter any bugs, please open an issue.
843
843
 
844
844
  ## Citation
845
845
 
@@ -971,9 +971,9 @@ moleditpy
971
971
  * **化学計算 (RDKit / Open Babel):** 2DデータからRDKit分子オブジェクトを生成し、3D座標生成や分子特性計算を実行します。RDKitでの3D座標生成が失敗した際は、Open Babelにフォールバックします。重い計算処理は別スレッド (`QThread`) で実行し、GUIの応答性を維持しています。
972
972
  * **3D可視化 (PyVista / pyvistaqt):** RDKitのコンフォーマ座標からPyVistaのメッシュ(球や円柱)を生成して描画します。カスタムの`vtkInteractorStyle`を実装し、3Dビュー内での原子の直接的なドラッグ&ドロップ編集を可能にしています。
973
973
 
974
- ## ライセンス
974
+ ## ライセンス & 免責事項
975
975
 
976
- このプロジェクトは **GNU General Public License v3.0 (GPL-v3)** のもとで公開されています。詳細は `LICENSE` ファイルを参照してください。
976
+ このプロジェクトは GNU General Public License v3.0 (GPLv3) のもとでライセンスされています。詳細は [LICENSE](LICENSE) ファイルを参照してください。オープンソースソフトウェアとして、本ソフトウェアは「現状のまま」提供され、いかなる明示または黙示の保証も行いません。また、本ソフトウェアを使用した結果について、作者は一切の責任や義務を負いません。出力結果は慎重に検証されていますが、学術論文の作成など重要な用途においては、ユーザーご自身で結果を独立して確認および検証することを強くお勧めします。バグに遭遇した場合は、Issueを作成してください。
977
977
 
978
978
  ## 引用
979
979
 
@@ -13,7 +13,21 @@ DOI: 10.5281/zenodo.17268532
13
13
  from __future__ import annotations
14
14
  import math
15
15
  from collections import deque
16
- from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
16
+ from typing import (
17
+ TYPE_CHECKING,
18
+ Any,
19
+ Callable,
20
+ Dict,
21
+ Iterable,
22
+ List,
23
+ Optional,
24
+ Set,
25
+ Tuple,
26
+ Union,
27
+ )
28
+
29
+ if TYPE_CHECKING:
30
+ from rdkit import Chem
17
31
 
18
32
  import numpy as np
19
33
 
@@ -82,7 +96,7 @@ def calc_angle_deg(
82
96
 
83
97
 
84
98
  def get_connected_group(
85
- mol: Any, start_atom: int, exclude: Optional[int] = None
99
+ mol: Chem.Mol, start_atom: int, exclude: Optional[int] = None
86
100
  ) -> Set[int]:
87
101
  """Return the set of atom indices reachable from *start_atom*
88
102
  without passing through *exclude*.
@@ -422,7 +436,9 @@ def is_problematic_valence(
422
436
 
423
437
 
424
438
  def inject_ez_stereo_to_mol_block(
425
- mol_block: str, rdkit_mol: Any, bonds_data: Dict[Tuple[int, int], Any]
439
+ mol_block: str,
440
+ rdkit_mol: Chem.Mol,
441
+ bonds_data: Dict[Tuple[int, int], Dict[str, Any]],
426
442
  ) -> str:
427
443
  """Generate a modified MOL block with 'M CFG' lines for E/Z stereochemistry.
428
444
 
@@ -504,7 +520,7 @@ def identify_valence_problems(
504
520
  problem_atom_ids = []
505
521
 
506
522
  # Pre-calculate bond orders per atom
507
- bond_orders: Dict[int, int] = {}
523
+ bond_orders: Dict[int, float] = {}
508
524
  for (id1, id2), bond in bonds_data.items():
509
525
  order = bond.get("order", 1)
510
526
  bond_orders[id1] = bond_orders.get(id1, 0) + order
@@ -521,7 +537,7 @@ def identify_valence_problems(
521
537
  return problem_atom_ids
522
538
 
523
539
 
524
- def optimize_2d_coords(mol: Any) -> Dict[int, Tuple[float, float]]:
540
+ def optimize_2d_coords(mol: Chem.Mol) -> Dict[int, Tuple[float, float]]:
525
541
  """Generate 2D coordinates using RDKit and return a map of (x, y) tuples."""
526
542
  from rdkit.Chem import AllChem
527
543
 
@@ -572,7 +588,7 @@ def resolve_2d_overlaps(
572
588
  adjacency_list: Dict[int, List[int]],
573
589
  overlap_threshold: float = 0.5,
574
590
  move_distance: float = 20,
575
- has_bond_check_func: Optional[Any] = None,
591
+ has_bond_check_func: Optional[Callable[[int, int], bool]] = None,
576
592
  ) -> List[Tuple[Set[int], Tuple[float, float]]]:
577
593
  """Detect and resolve overlapping atom groups in 2D.
578
594
 
@@ -601,13 +617,13 @@ def resolve_2d_overlaps(
601
617
  # Union-Find for overlap groups
602
618
  parent = {aid: aid for aid in atom_ids}
603
619
 
604
- def find_set(aid: Any) -> Any:
620
+ def find_set(aid: int) -> int:
605
621
  if parent[aid] == aid:
606
622
  return aid
607
623
  parent[aid] = find_set(parent[aid])
608
624
  return parent[aid]
609
625
 
610
- def unite_sets(aid1: Any, aid2: Any) -> None:
626
+ def unite_sets(aid1: int, aid2: int) -> None:
611
627
  root1 = find_set(aid1)
612
628
  root2 = find_set(aid2)
613
629
  if root1 != root2:
@@ -11,10 +11,12 @@ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  import ctypes
14
- import sys
15
- import argparse
14
+ import json
16
15
  import logging
16
+ import logging.handlers
17
17
  import os
18
+ import sys
19
+ import argparse
18
20
  from typing import Any
19
21
 
20
22
  from .utils.constants import VERSION
@@ -46,22 +48,55 @@ def _qt_message_handler(mode: QtMsgType, _context: Any, message: str) -> None:
46
48
  logging.log(_QT_LOG_LEVEL.get(mode, logging.WARNING), "Qt: %s", message)
47
49
 
48
50
 
51
+ def _read_startup_log_settings() -> tuple[bool, bool]:
52
+ """Read log_to_file and log_level_debug from settings.json before Qt starts.
53
+
54
+ Returns (log_to_file, log_level_debug). Falls back to (False, False) on any error.
55
+ """
56
+ settings_path = os.path.join(os.path.expanduser("~"), ".moleditpy", "settings.json")
57
+ try:
58
+ with open(settings_path, encoding="utf-8") as f:
59
+ data = json.load(f)
60
+ return bool(data.get("log_to_file", False)), bool(
61
+ data.get("log_level_debug", False)
62
+ )
63
+ except (OSError, json.JSONDecodeError, ValueError):
64
+ return False, False
65
+
66
+
49
67
  def setup_logging() -> None:
50
68
  """Configure root logger and install a global unhandled-exception handler."""
69
+ log_to_file, log_level_debug = _read_startup_log_settings()
70
+ level = logging.DEBUG if log_level_debug else logging.INFO
71
+ fmt = "%(asctime)s [%(levelname)s] %(name)s (%(pathname)s:%(lineno)d): %(message)s"
72
+
51
73
  logging.basicConfig(
52
- level=logging.INFO,
53
- format="%(asctime)s [%(levelname)s] %(name)s (%(pathname)s:%(lineno)d): %(message)s",
74
+ level=level,
75
+ format=fmt,
54
76
  stream=sys.stdout,
55
77
  force=True,
56
78
  )
57
79
 
80
+ if log_to_file:
81
+ log_dir = os.path.join(os.path.expanduser("~"), ".moleditpy")
82
+ try:
83
+ os.makedirs(log_dir, exist_ok=True)
84
+ log_path = os.path.join(log_dir, "moleditpy_linux.log")
85
+ fh = logging.handlers.RotatingFileHandler(
86
+ log_path, maxBytes=1_048_576, backupCount=3, encoding="utf-8"
87
+ )
88
+ fh.setLevel(level)
89
+ fh.setFormatter(logging.Formatter(fmt))
90
+ logging.getLogger().addHandler(fh)
91
+ logging.info("File logging enabled: %s", log_path)
92
+ except OSError as e:
93
+ logging.warning("Could not open log file: %s", e)
94
+
58
95
  def handle_exception(exc_type: Any, exc_value: Any, exc_traceback: Any) -> None:
59
96
  """Log unhandled exceptions using the configured logging system."""
60
97
  if issubclass(exc_type, KeyboardInterrupt):
61
- # Allow keyboard interrupt to exit normally
62
98
  sys.__excepthook__(exc_type, exc_value, exc_traceback)
63
99
  return
64
-
65
100
  logging.error(
66
101
  "Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
67
102
  )
@@ -10,6 +10,7 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
+ import logging
13
14
  from typing import Any, Callable, List, Optional, Union
14
15
 
15
16
 
@@ -173,10 +174,11 @@ class PluginContext:
173
174
  """Switch the application UI layout to 3D viewer mode (Public API)."""
174
175
  mw = self.get_main_window()
175
176
  if mw is not None and hasattr(mw, "ui_manager"):
176
- if hasattr(mw.ui_manager, "enter_3d_viewer_mode"):
177
- mw.ui_manager.enter_3d_viewer_mode()
178
- elif hasattr(mw.ui_manager, "enter_3d_viewer_ui_mode"):
179
- mw.ui_manager.enter_3d_viewer_ui_mode()
177
+ fn = getattr(mw.ui_manager, "enter_3d_viewer_mode", None) or getattr(
178
+ mw.ui_manager, "enter_3d_viewer_ui_mode", None
179
+ )
180
+ if fn:
181
+ fn()
180
182
 
181
183
  def enter_3d_mode(self) -> None:
182
184
  """Switch UI layout to 3D viewer mode. Alias for enter_3d_viewer_mode."""
@@ -240,16 +242,11 @@ class PluginContext:
240
242
  """Force the 3D window to redraw using the current molecule."""
241
243
  mw = self.get_main_window()
242
244
  if mw and hasattr(mw, "view_3d_manager"):
243
- mol = getattr(mw.view_3d_manager, "current_mol", None)
245
+ mol = mw.view_3d_manager.current_mol
244
246
  if mol:
245
247
  mw.view_3d_manager.draw_molecule_3d(mol)
246
- else:
247
- # Also redraw/clear plotter if no molecule
248
- if (
249
- hasattr(mw.view_3d_manager, "plotter")
250
- and mw.view_3d_manager.plotter
251
- ):
252
- mw.view_3d_manager.plotter.render()
248
+ elif mw.view_3d_manager.plotter:
249
+ mw.view_3d_manager.plotter.render()
253
250
 
254
251
  def reset_3d_camera(self) -> None:
255
252
  """Zoom in and re-center the 3D viewport to fit the current molecule."""
@@ -370,7 +367,7 @@ class PluginContext:
370
367
  default: Value to return if the setting is not found.
371
368
  """
372
369
  mw = self.get_main_window()
373
- if mw and hasattr(mw, "init_manager") and hasattr(mw.init_manager, "settings"):
370
+ if mw and hasattr(mw, "init_manager"):
374
371
  namespaced = f"plugin.{self._plugin_name}.{key}"
375
372
  return mw.init_manager.settings.get(namespaced, default)
376
373
  return default
@@ -387,11 +384,10 @@ class PluginContext:
387
384
  value: Value to store (must be JSON-serializable).
388
385
  """
389
386
  mw = self.get_main_window()
390
- if mw and hasattr(mw, "init_manager") and hasattr(mw.init_manager, "settings"):
387
+ if mw and hasattr(mw, "init_manager"):
391
388
  namespaced = f"plugin.{self._plugin_name}.{key}"
392
389
  mw.init_manager.settings[namespaced] = value
393
- if hasattr(mw.init_manager, "settings_dirty"):
394
- mw.init_manager.settings_dirty = True
390
+ mw.init_manager.settings_dirty = True
395
391
 
396
392
  def mark_project_modified(self) -> None:
397
393
  """Mark the current project as having unsaved changes and update the window title."""
@@ -399,8 +395,9 @@ class PluginContext:
399
395
  if mw and hasattr(mw, "state_manager"):
400
396
  try:
401
397
  mw.state_manager.has_unsaved_changes = True
402
- if hasattr(mw.state_manager, "update_window_title"):
403
- mw.state_manager.update_window_title()
398
+ fn = getattr(mw.state_manager, "update_window_title", None)
399
+ if fn:
400
+ fn()
404
401
  except Exception:
405
402
  pass
406
403
 
@@ -414,14 +411,16 @@ class PluginContext:
414
411
  if mw is None:
415
412
  return
416
413
  if hasattr(mw, "state_manager"):
417
- if hasattr(mw.state_manager, "update_realtime_info"):
418
- mw.state_manager.update_realtime_info()
419
- if hasattr(mw.state_manager, "update_window_title"):
420
- mw.state_manager.update_window_title()
421
- if hasattr(mw, "edit_actions_manager") and hasattr(
422
- mw.edit_actions_manager, "update_undo_redo_actions"
423
- ):
424
- mw.edit_actions_manager.update_undo_redo_actions()
414
+ fn = getattr(mw.state_manager, "update_realtime_info", None)
415
+ if fn:
416
+ fn()
417
+ fn = getattr(mw.state_manager, "update_window_title", None)
418
+ if fn:
419
+ fn()
420
+ if hasattr(mw, "edit_actions_manager"):
421
+ fn = getattr(mw.edit_actions_manager, "update_undo_redo_actions", None)
422
+ if fn:
423
+ fn()
425
424
 
426
425
  def fit_3d_view(self) -> None:
427
426
  """Zoom and re-center the 3D viewport to fit the current molecule."""
@@ -489,8 +488,10 @@ class PluginContext:
489
488
  mw = self.get_main_window()
490
489
  if mw and hasattr(mw, "init_manager"):
491
490
  scene = getattr(mw.init_manager, "scene", None)
492
- if scene is not None and hasattr(scene, "update_all_items"):
493
- scene.update_all_items()
491
+ if scene is not None:
492
+ fn = getattr(scene, "update_all_items", None)
493
+ if fn:
494
+ fn()
494
495
 
495
496
  def load_from_smiles(self, smiles: str) -> None:
496
497
  """Add a molecule from a SMILES string to the 2D editor."""
@@ -518,6 +519,7 @@ class PluginContext:
518
519
 
519
520
  return "\n".join(xyz_lines)
520
521
  except Exception:
522
+ logging.debug("to_xyz_block failed", exc_info=True)
521
523
  return None
522
524
 
523
525
 
@@ -541,8 +543,8 @@ class Plugin3DController:
541
543
  v3d = self._get_v3d()
542
544
  if v3d:
543
545
  v3d.update_atom_color_override(atom_index, color_hex)
544
- if hasattr(self._mw, "plotter") and self._mw.plotter:
545
- self._mw.plotter.render()
546
+ if v3d.plotter:
547
+ v3d.plotter.render()
546
548
 
547
549
  def set_bond_color(self, bond_index: int, color_hex: str) -> None:
548
550
  """
@@ -555,8 +557,8 @@ class Plugin3DController:
555
557
  v3d = self._get_v3d()
556
558
  if v3d:
557
559
  v3d.update_bond_color_override(bond_index, color_hex)
558
- if hasattr(self._mw, "plotter") and self._mw.plotter:
559
- self._mw.plotter.render()
560
+ if v3d.plotter:
561
+ v3d.plotter.render()
560
562
 
561
563
  def set_bond_color_by_atoms(
562
564
  self, atom_idx1: int, atom_idx2: int, color_hex: str
@@ -569,7 +571,8 @@ class Plugin3DController:
569
571
  atom_idx2: Second RDKit atom index.
570
572
  color_hex: Hex string e.g., "#00FF00".
571
573
  """
572
- mol = getattr(self._mw, "current_mol", None)
574
+ v3d = self._get_v3d()
575
+ mol = v3d.current_mol if v3d else None
573
576
  if not mol:
574
577
  return
575
578
 
@@ -213,7 +213,7 @@ class PluginManagerWindow(QDialog):
213
213
  "Success",
214
214
  f"Removed '{plugin.get('name', 'Unknown')}'.",
215
215
  )
216
- except (AttributeError, RuntimeError, ValueError) as e:
216
+ except OSError as e:
217
217
  QMessageBox.critical(
218
218
  self, "Error", f"Failed to delete plugin: {e}"
219
219
  )
@@ -542,13 +542,9 @@ class StateManager:
542
542
  # 3D viewer mode
543
543
  is_3d_mode = json_data.get("is_3d_viewer_mode", False)
544
544
  # Restore last successful optimization method if present in file
545
- try:
546
- method = json_data.get("last_successful_optimization_method", None)
547
- with contextlib.suppress(AttributeError, RuntimeError, TypeError):
548
- self.host.set_last_successful_optimization_method(method)
549
- except (AttributeError, RuntimeError, TypeError):
550
- # Safe defensive fallback catching AttributeError, RuntimeError, TypeError
551
- pass
545
+ method = json_data.get("last_successful_optimization_method", None)
546
+ with contextlib.suppress(AttributeError, RuntimeError, TypeError):
547
+ self.host.set_last_successful_optimization_method(method)
552
548
 
553
549
  # Plugin State Restoration (Phase 3)
554
550
  self._preserved_plugin_data = {} # Reset preserved data on new load
@@ -11,7 +11,7 @@ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  from __future__ import annotations
14
- from typing import Any, List, Optional
14
+ from typing import TYPE_CHECKING, Any, List, Optional
15
15
 
16
16
  from PyQt6.QtCore import QPointF, QRectF, Qt
17
17
  from PyQt6.QtGui import (
@@ -23,7 +23,12 @@ from PyQt6.QtGui import (
23
23
  QPainterPath,
24
24
  QPen,
25
25
  )
26
- from PyQt6.QtWidgets import QGraphicsItem, QWidget
26
+ from PyQt6.QtWidgets import (
27
+ QGraphicsItem,
28
+ QGraphicsSceneHoverEvent,
29
+ QStyleOptionGraphicsItem,
30
+ QWidget,
31
+ )
27
32
 
28
33
  from ..utils.constants import (
29
34
  ATOM_RADIUS,
@@ -34,6 +39,9 @@ from ..utils.constants import (
34
39
  )
35
40
  from ..utils.sip_isdeleted_safe import sip_isdeleted_safe
36
41
 
42
+ if TYPE_CHECKING:
43
+ from .bond_item import BondItem
44
+
37
45
 
38
46
  class AtomItem(QGraphicsItem):
39
47
  """2D scene item representing a single atom in the molecule editor."""
@@ -46,7 +54,7 @@ class AtomItem(QGraphicsItem):
46
54
  self.symbol: str = symbol
47
55
  self.charge: int = charge
48
56
  self.radical: int = radical
49
- self.bonds: List[Any] = []
57
+ self.bonds: List[BondItem] = []
50
58
  self.chiral_label: Optional[str] = None
51
59
 
52
60
  self.setPos(pos)
@@ -67,16 +75,24 @@ class AtomItem(QGraphicsItem):
67
75
  """Refresh font, color, and visibility based on current scene settings."""
68
76
  if sip_isdeleted_safe(self):
69
77
  return
70
- # Allow updating font preference dynamically
71
78
  font_size = 20
72
79
  font_family = FONT_FAMILY
80
+ font_bold = True
81
+ font_italic = False
82
+ font_underline = False
73
83
 
74
84
  scene = self.scene()
75
85
  if scene is not None and hasattr(scene, "get_setting"):
76
86
  font_size = scene.get_setting("atom_font_size_2d", 20)
77
87
  font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
78
-
79
- self.font = QFont(font_family, font_size, FONT_WEIGHT_BOLD)
88
+ font_bold = scene.get_setting("atom_font_bold_2d", True)
89
+ font_italic = scene.get_setting("atom_font_italic_2d", False)
90
+ font_underline = scene.get_setting("atom_font_underline_2d", False)
91
+
92
+ weight = QFont.Weight.Bold if font_bold else QFont.Weight.Normal
93
+ self.font = QFont(font_family, font_size, weight)
94
+ self.font.setItalic(font_italic)
95
+ self.font.setUnderline(font_underline)
80
96
  self.prepareGeometryChange()
81
97
 
82
98
  self.is_visible = not (
@@ -89,16 +105,23 @@ class AtomItem(QGraphicsItem):
89
105
 
90
106
  def boundingRect(self) -> QRectF:
91
107
  """Calculate the bounding rectangle for the atom item."""
92
- # --- Calculate text position and size using logic matching paint() ---
93
- # Get dynamic font size and family
94
108
  font_size = 20
95
109
  font_family = FONT_FAMILY
110
+ font_bold = True
111
+ font_italic = False
112
+ font_underline = False
96
113
  scene = self.scene()
97
114
  if scene is not None and hasattr(scene, "get_setting"):
98
115
  font_size = scene.get_setting("atom_font_size_2d", 20)
99
116
  font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
100
-
101
- font = QFont(font_family, font_size, FONT_WEIGHT_BOLD)
117
+ font_bold = scene.get_setting("atom_font_bold_2d", True)
118
+ font_italic = scene.get_setting("atom_font_italic_2d", False)
119
+ font_underline = scene.get_setting("atom_font_underline_2d", False)
120
+
121
+ weight = QFont.Weight.Bold if font_bold else QFont.Weight.Normal
122
+ font = QFont(font_family, font_size, weight)
123
+ font.setItalic(font_italic)
124
+ font.setUnderline(font_underline)
102
125
  fm = QFontMetricsF(font)
103
126
 
104
127
  hydrogen_part = ""
@@ -207,13 +230,21 @@ class AtomItem(QGraphicsItem):
207
230
 
208
231
  font_size = 20
209
232
  font_family = FONT_FAMILY
233
+ font_bold = True
234
+ font_italic = False
235
+ font_underline = False
210
236
  scene = self.scene()
211
- if scene is not None:
212
- if hasattr(scene, "get_setting"):
213
- font_size = scene.get_setting("atom_font_size_2d", 20)
214
- font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
215
-
216
- font = QFont(font_family, font_size, FONT_WEIGHT_BOLD)
237
+ if scene is not None and hasattr(scene, "get_setting"):
238
+ font_size = scene.get_setting("atom_font_size_2d", 20)
239
+ font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
240
+ font_bold = scene.get_setting("atom_font_bold_2d", True)
241
+ font_italic = scene.get_setting("atom_font_italic_2d", False)
242
+ font_underline = scene.get_setting("atom_font_underline_2d", False)
243
+
244
+ weight = QFont.Weight.Bold if font_bold else QFont.Weight.Normal
245
+ font = QFont(font_family, font_size, weight)
246
+ font.setItalic(font_italic)
247
+ font.setUnderline(font_underline)
217
248
  fm = QFontMetricsF(font)
218
249
 
219
250
  hydrogen_part = ""
@@ -292,7 +323,7 @@ class AtomItem(QGraphicsItem):
292
323
  def paint(
293
324
  self,
294
325
  painter: Optional[QPainter],
295
- option: Any,
326
+ option: QStyleOptionGraphicsItem,
296
327
  widget: Optional[QWidget] = None,
297
328
  ) -> None:
298
329
  """Paint the atom symbol and its associated labels (charge, radical)."""
@@ -484,14 +515,14 @@ class AtomItem(QGraphicsItem):
484
515
  self.update_style()
485
516
  return res
486
517
 
487
- def hoverEnterEvent(self, event: Any) -> None:
518
+ def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent) -> None:
488
519
  """Highlight the atom on mouse hover."""
489
520
  # Enable highlight on hover regardless of scene mode
490
521
  self.hovered = True
491
522
  self.update()
492
523
  super().hoverEnterEvent(event)
493
524
 
494
- def hoverLeaveEvent(self, event: Any) -> None:
525
+ def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent) -> None:
495
526
  """Remove hover highlight when the mouse leaves."""
496
527
  if self.hovered:
497
528
  self.hovered = False