MoleditPy-linux 3.6.6__tar.gz → 4.0.0__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 (81) hide show
  1. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/PKG-INFO +6 -3
  2. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/README.md +5 -2
  3. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/pyproject.toml +1 -1
  4. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/MoleditPy_linux.egg-info/PKG-INFO +6 -3
  5. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/MoleditPy_linux.egg-info/SOURCES.txt +1 -0
  6. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/__init__.py +1 -1
  7. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/core/molecular_data.py +6 -98
  8. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/main.py +6 -14
  9. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/plugins/plugin_interface.py +151 -2
  10. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/plugins/plugin_manager.py +26 -70
  11. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/plugins/plugin_manager_window.py +3 -1
  12. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/about_dialog.py +2 -4
  13. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/align_plane_dialog.py +3 -9
  14. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/alignment_dialog.py +5 -14
  15. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/analysis_window.py +6 -4
  16. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/angle_dialog.py +22 -14
  17. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/app_state.py +132 -340
  18. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/atom_item.py +16 -44
  19. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/atom_picking.py +6 -8
  20. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/base_picking_dialog.py +5 -18
  21. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/bond_item.py +58 -88
  22. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/bond_length_dialog.py +17 -6
  23. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/calculation_worker.py +35 -69
  24. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/color_settings_dialog.py +25 -89
  25. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/compute_logic.py +94 -139
  26. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +12 -8
  27. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/custom_interactor_style.py +100 -98
  28. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +19 -16
  29. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/dialog_logic.py +30 -67
  30. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/dihedral_dialog.py +20 -14
  31. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/edit_3d_logic.py +46 -83
  32. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/edit_actions_logic.py +147 -244
  33. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/export_logic.py +16 -40
  34. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/geometry_base_dialog.py +2 -13
  35. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/io_logic.py +91 -155
  36. moleditpy_linux-4.0.0/src/moleditpy_linux/ui/main_window.py +318 -0
  37. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/main_window_init.py +214 -631
  38. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/mirror_dialog.py +3 -2
  39. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/molecular_scene_handler.py +67 -96
  40. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/molecule_scene.py +218 -114
  41. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/move_group_dialog.py +25 -22
  42. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +7 -12
  43. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/periodic_table_dialog.py +1 -4
  44. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/planarize_dialog.py +6 -4
  45. moleditpy_linux-4.0.0/src/moleditpy_linux/ui/plugin_menu_manager.py +463 -0
  46. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/settings_dialog.py +16 -32
  47. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +6 -0
  48. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +7 -0
  49. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +8 -2
  50. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +4 -4
  51. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/string_importers.py +2 -14
  52. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/template_preview_item.py +1 -4
  53. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/template_preview_view.py +5 -4
  54. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/translation_dialog.py +1 -4
  55. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/ui_manager.py +75 -115
  56. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/user_template_dialog.py +8 -20
  57. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/view_3d_logic.py +133 -213
  58. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/utils/constants.py +5 -3
  59. moleditpy_linux-3.6.6/src/moleditpy_linux/ui/main_window.py +0 -122
  60. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/LICENSE +0 -0
  61. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/setup.cfg +0 -0
  62. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  63. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  64. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  65. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  66. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/__main__.py +0 -0
  67. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  68. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/assets/icon.icns +0 -0
  69. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/assets/icon.ico +0 -0
  70. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/assets/icon.png +0 -0
  71. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/core/__init__.py +0 -0
  72. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  73. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/plugins/__init__.py +0 -0
  74. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/__init__.py +0 -0
  75. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  76. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  77. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  78. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/utils/__init__.py +0 -0
  79. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/utils/default_settings.py +0 -0
  80. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  81. {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.0}/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: 3.6.6
3
+ Version: 4.0.0
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
@@ -709,8 +709,7 @@ Dynamic: license-file
709
709
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
710
710
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
711
711
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
712
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage->80%25-green)
713
- ![Overall Coverage](https://img.shields.io/badge/coverage->75%25-green)
712
+ ![Overall Coverage](https://img.shields.io/badge/coverage->80%25-green)
714
713
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
715
714
  ![Pylint Score](https://img.shields.io/badge/pylint->9%2F10-brightgreen)
716
715
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
@@ -850,6 +849,8 @@ If you use this software in your work, please cite it as follows:
850
849
  Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
851
850
  ```
852
851
 
852
+ Additionally, please cite the plugins you used.
853
+
853
854
  -----
854
855
 
855
856
  <div id="japanese"></div>
@@ -981,3 +982,5 @@ moleditpy
981
982
  ```
982
983
  Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
983
984
  ```
985
+
986
+ また、使用したプラグインの引用もお願いいたします。
@@ -6,8 +6,7 @@
6
6
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
7
7
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
8
8
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
9
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage->80%25-green)
10
- ![Overall Coverage](https://img.shields.io/badge/coverage->75%25-green)
9
+ ![Overall Coverage](https://img.shields.io/badge/coverage->80%25-green)
11
10
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
12
11
  ![Pylint Score](https://img.shields.io/badge/pylint->9%2F10-brightgreen)
13
12
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
@@ -147,6 +146,8 @@ If you use this software in your work, please cite it as follows:
147
146
  Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
148
147
  ```
149
148
 
149
+ Additionally, please cite the plugins you used.
150
+
150
151
  -----
151
152
 
152
153
  <div id="japanese"></div>
@@ -278,3 +279,5 @@ moleditpy
278
279
  ```
279
280
  Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
280
281
  ```
282
+
283
+ また、使用したプラグインの引用もお願いいたします。
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "3.6.6"
8
+ version = "4.0.0"
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: 3.6.6
3
+ Version: 4.0.0
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
@@ -709,8 +709,7 @@ Dynamic: license-file
709
709
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
710
710
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
711
711
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
712
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage->80%25-green)
713
- ![Overall Coverage](https://img.shields.io/badge/coverage->75%25-green)
712
+ ![Overall Coverage](https://img.shields.io/badge/coverage->80%25-green)
714
713
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
715
714
  ![Pylint Score](https://img.shields.io/badge/pylint->9%2F10-brightgreen)
716
715
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
@@ -850,6 +849,8 @@ If you use this software in your work, please cite it as follows:
850
849
  Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
851
850
  ```
852
851
 
852
+ Additionally, please cite the plugins you used.
853
+
853
854
  -----
854
855
 
855
856
  <div id="japanese"></div>
@@ -981,3 +982,5 @@ moleditpy
981
982
  ```
982
983
  Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
983
984
  ```
985
+
986
+ また、使用したプラグインの引用もお願いいたします。
@@ -56,6 +56,7 @@ src/moleditpy_linux/ui/move_group_dialog.py
56
56
  src/moleditpy_linux/ui/move_selected_atoms_dialog.py
57
57
  src/moleditpy_linux/ui/periodic_table_dialog.py
58
58
  src/moleditpy_linux/ui/planarize_dialog.py
59
+ src/moleditpy_linux/ui/plugin_menu_manager.py
59
60
  src/moleditpy_linux/ui/settings_dialog.py
60
61
  src/moleditpy_linux/ui/string_importers.py
61
62
  src/moleditpy_linux/ui/template_preview_item.py
@@ -12,7 +12,7 @@ DOI: 10.5281/zenodo.17268532
12
12
  Top-level package for moleditpy_linux.
13
13
  """
14
14
 
15
- import importlib.util
15
+ import importlib.util # noqa: F401
16
16
  from .utils.constants import VERSION as __version__ # noqa: F401
17
17
 
18
18
  try:
@@ -15,10 +15,7 @@ import logging
15
15
  from typing import Any, Dict, List, Optional, Tuple, Union
16
16
  from rdkit import Chem
17
17
 
18
- try:
19
- from ..utils.constants import ANGSTROM_PER_PIXEL
20
- except ImportError:
21
- from moleditpy_linux.utils.constants import ANGSTROM_PER_PIXEL
18
+ from ..utils.constants import ANGSTROM_PER_PIXEL
22
19
 
23
20
 
24
21
  class PointTuple(tuple):
@@ -35,12 +32,12 @@ class MolecularData:
35
32
  atoms: Dict[int, Dict[str, Any]]
36
33
  bonds: Dict[Tuple[int, int], Dict[str, Any]]
37
34
  adjacency_list: Dict[int, List[int]]
38
- _next_atom_id: int
35
+ next_atom_id: int
39
36
 
40
37
  def __init__(self) -> None:
41
38
  self.atoms = {}
42
39
  self.bonds = {}
43
- self._next_atom_id = 0
40
+ self.next_atom_id = 0
44
41
  self.adjacency_list = {}
45
42
 
46
43
  def add_atom(
@@ -50,7 +47,7 @@ class MolecularData:
50
47
  charge: int = 0,
51
48
  radical: int = 0,
52
49
  ) -> int:
53
- atom_id = self._next_atom_id
50
+ atom_id = self.next_atom_id
54
51
  # Internalize position as raw floats to decouple from UI types (QPointF)
55
52
  if hasattr(pos, "x") and hasattr(pos, "y"):
56
53
  raw_pos = PointTuple((float(pos.x()), float(pos.y())))
@@ -60,12 +57,11 @@ class MolecularData:
60
57
  self.atoms[atom_id] = {
61
58
  "symbol": symbol,
62
59
  "pos": raw_pos,
63
- "item": None,
64
60
  "charge": charge,
65
61
  "radical": radical,
66
62
  }
67
63
  self.adjacency_list[atom_id] = []
68
- self._next_atom_id += 1
64
+ self.next_atom_id += 1
69
65
  return atom_id
70
66
 
71
67
  def set_atom_pos(self, atom_id: int, pos: Union[Any, Tuple[float, float]]) -> None:
@@ -87,7 +83,7 @@ class MolecularData:
87
83
  if id1 > id2:
88
84
  id1, id2 = id2, id1
89
85
 
90
- bond_data = {"order": order, "stereo": stereo, "item": None}
86
+ bond_data = {"order": order, "stereo": stereo}
91
87
 
92
88
  # Check if it's a new bond, considering reverse direction keys.
93
89
  is_new_bond = (id1, id2) not in self.bonds and (id2, id1) not in self.bonds
@@ -321,94 +317,6 @@ class MolecularData:
321
317
  Chem.AssignStereochemistry(final_mol, cleanIt=False, force=False)
322
318
  return final_mol
323
319
 
324
- def update_ring_info_2d(self) -> None:
325
- """Update is_in_ring and ring_center for all BondItems based on 2D topology."""
326
- if not self.atoms or not self.bonds:
327
- return
328
-
329
- # 1. Generate RDKit molecule for topology analysis
330
- mol = self.to_rdkit_mol(use_2d_stereo=False)
331
- if not mol:
332
- # Fallback: reset all ring info if molecule generation fails
333
- for bond_data in self.bonds.values():
334
- bond_item = bond_data.get("item")
335
- if bond_item:
336
- bond_item.is_in_ring = False
337
- bond_item.ring_center = None
338
- return
339
-
340
- # 2. Extract ring information
341
- ring_info = mol.GetRingInfo()
342
- atom_rings = ring_info.AtomRings()
343
- bond_rings = ring_info.BondRings()
344
-
345
- # 3. Create mapping from RDKit atom index to editor atom item
346
- rdkit_idx_to_item = {}
347
- for atom in mol.GetAtoms():
348
- if atom.HasProp("_original_atom_id"):
349
- orig_id = atom.GetIntProp("_original_atom_id")
350
- if orig_id in self.atoms:
351
- rdkit_idx_to_item[atom.GetIdx()] = self.atoms[orig_id]["item"]
352
-
353
- # 4. Map RDKit bond index to editor bond item
354
- rdkit_bond_idx_to_item = {}
355
- for bidx, rdkit_bond in enumerate(mol.GetBonds()):
356
- a1_idx = rdkit_bond.GetBeginAtomIdx()
357
- a2_idx = rdkit_bond.GetEndAtomIdx()
358
- if a1_idx in rdkit_idx_to_item and a2_idx in rdkit_idx_to_item:
359
- # Find corresponding editor bond item
360
- # This is slightly expensive but done once per update
361
- item1 = rdkit_idx_to_item[a1_idx]
362
- item2 = rdkit_idx_to_item[a2_idx]
363
- id1, id2 = item1.atom_id, item2.atom_id
364
- key = (id1, id2) if (id1, id2) in self.bonds else (id2, id1)
365
- if key in self.bonds:
366
- rdkit_bond_idx_to_item[bidx] = self.bonds[key].get("item")
367
-
368
- # 5. Initialize/Reset all bond items and track best ring size
369
- bond_to_best_size: Dict[
370
- int, int
371
- ] = {} # bond_item_id -> smallest_ring_size_found
372
- for bond_data in self.bonds.values():
373
- bond_item = bond_data.get("item")
374
- if bond_item:
375
- bond_item.is_in_ring = False
376
- bond_item.ring_center = None
377
-
378
- # 6. Apply ring information
379
- for a_ring, b_ring in zip(atom_rings, bond_rings):
380
- ring_size = len(a_ring)
381
- # Calculate ring center (geometric mean of atom positions)
382
- positions = []
383
- for aidx in a_ring:
384
- item = rdkit_idx_to_item.get(aidx)
385
- if item and hasattr(item, "pos"):
386
- pos = item.pos()
387
- if pos is not None:
388
- positions.append(pos)
389
-
390
- if not positions:
391
- continue
392
-
393
- center_x = sum(p.x() for p in positions) / len(positions)
394
- center_y = sum(p.y() for p in positions) / len(positions)
395
- ring_center = (center_x, center_y) # Use tuple (x, y) instead of QPointF
396
-
397
- # Update all bonds in this ring
398
- for bidx in b_ring:
399
- bond_item = rdkit_bond_idx_to_item.get(bidx)
400
- if bond_item:
401
- bond_item.is_in_ring = True
402
- # Explicitly prioritize smaller rings for double bond shift logic.
403
- # This ensures the double bond is drawn inside the smaller ring in fused systems.
404
- item_id = id(bond_item)
405
- if (
406
- item_id not in bond_to_best_size
407
- or ring_size < bond_to_best_size[item_id]
408
- ):
409
- bond_item.ring_center = ring_center
410
- bond_to_best_size[item_id] = ring_size
411
-
412
320
  def to_mol_block(self) -> Optional[str]:
413
321
  mol = self.to_rdkit_mol()
414
322
  if mol:
@@ -17,23 +17,13 @@ import logging
17
17
  import os
18
18
  from typing import Any
19
19
 
20
- try:
21
- from .utils.constants import VERSION
22
- except ImportError:
23
- # Add the parent directory (src) to sys.path so 'moleditpy_linux.*' imports work
24
- src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
25
- if src_dir not in sys.path:
26
- sys.path.insert(0, src_dir)
27
- from moleditpy_linux.utils.constants import VERSION
20
+ from .utils.constants import VERSION
28
21
 
29
22
  # VERSION is resolved above (before Qt) so --version works without launching the app.
30
23
 
31
24
  from PyQt6.QtWidgets import QApplication
32
25
 
33
- try:
34
- from .ui.main_window import MainWindow
35
- except ImportError:
36
- from moleditpy_linux.ui.main_window import MainWindow
26
+ from .ui.main_window import MainWindow
37
27
 
38
28
 
39
29
  def setup_logging() -> None:
@@ -103,7 +93,7 @@ def main() -> None:
103
93
  from .plugins.plugin_manager import PluginManager
104
94
 
105
95
  pm = PluginManager()
106
- sha256 = pm._compute_sha256(plugin_path)
96
+ sha256 = pm.compute_sha256(plugin_path)
107
97
 
108
98
  # Extract metadata
109
99
  metadata_file = plugin_path
@@ -155,7 +145,9 @@ def main() -> None:
155
145
  from PyQt6.QtCore import QTimer
156
146
 
157
147
  QTimer.singleShot(100, lambda: window.setWindowIcon(window.windowIcon()))
158
- except Exception:
148
+ except (
149
+ Exception
150
+ ): # [COSMETIC] Icon refresh is best-effort; Qt timing errors are non-fatal.
159
151
  pass
160
152
 
161
153
  sys.exit(app.exec())
@@ -164,6 +164,24 @@ class PluginContext:
164
164
  """
165
165
  self._manager.show_status_message(message, timeout)
166
166
 
167
+ def rebuild_menus(self) -> None:
168
+ """Rebuild plugin-managed menus and toolbars to apply changes immediately (Public API)."""
169
+ if hasattr(self._manager, "rebuild_plugin_menus"):
170
+ self._manager.rebuild_plugin_menus()
171
+
172
+ def enter_3d_viewer_mode(self) -> None:
173
+ """Switch the application UI layout to 3D viewer mode (Public API)."""
174
+ mw = self.get_main_window()
175
+ 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()
180
+
181
+ def enter_3d_mode(self) -> None:
182
+ """Switch UI layout to 3D viewer mode. Alias for enter_3d_viewer_mode."""
183
+ self.enter_3d_viewer_mode()
184
+
167
185
  @property
168
186
  def current_mol(self) -> Any:
169
187
  """
@@ -310,8 +328,12 @@ class PluginContext:
310
328
 
311
329
  def register_3d_context_menu(self, callback: Callable, label: str) -> None:
312
330
  """Deprecated: This method does nothing. Kept for backward compatibility."""
313
- print(
314
- f"Warning: Plugin '{self._plugin_name}' uses deprecated 'register_3d_context_menu'. This API has been removed."
331
+ import warnings
332
+
333
+ warnings.warn(
334
+ f"Plugin '{self._plugin_name}' uses deprecated 'register_3d_context_menu'. This API has been removed.",
335
+ category=DeprecationWarning,
336
+ stacklevel=2,
315
337
  )
316
338
 
317
339
  def register_3d_style(
@@ -371,6 +393,133 @@ class PluginContext:
371
393
  if hasattr(mw.init_manager, "settings_dirty"):
372
394
  mw.init_manager.settings_dirty = True
373
395
 
396
+ def mark_project_modified(self) -> None:
397
+ """Mark the current project as having unsaved changes and update the window title."""
398
+ mw = self.get_main_window()
399
+ if mw and hasattr(mw, "state_manager"):
400
+ try:
401
+ mw.state_manager.has_unsaved_changes = True
402
+ if hasattr(mw.state_manager, "update_window_title"):
403
+ mw.state_manager.update_window_title()
404
+ except Exception:
405
+ pass
406
+
407
+ def refresh_ui(self) -> None:
408
+ """Refresh all UI state after modifying the molecule.
409
+
410
+ Calls update_realtime_info, update_undo_redo_actions, and update_window_title
411
+ in one shot. Call this at the end of any edit that changes atom/bond data.
412
+ """
413
+ mw = self.get_main_window()
414
+ if mw is None:
415
+ return
416
+ 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()
425
+
426
+ def fit_3d_view(self) -> None:
427
+ """Zoom and re-center the 3D viewport to fit the current molecule."""
428
+ mw = self.get_main_window()
429
+ if mw and hasattr(mw, "view_3d_manager"):
430
+ fit = getattr(mw.view_3d_manager, "fit_to_view", None)
431
+ if fit is not None:
432
+ fit()
433
+
434
+ def clear_canvas(self, push_to_undo: bool = True) -> None:
435
+ """Clear the 2D editor canvas.
436
+
437
+ Args:
438
+ push_to_undo: Whether to push the cleared state onto the undo stack
439
+ before clearing (default True).
440
+ """
441
+ mw = self.get_main_window()
442
+ if mw and hasattr(mw, "edit_actions_manager"):
443
+ clear = getattr(mw.edit_actions_manager, "clear_2d_editor", None)
444
+ if clear is not None:
445
+ clear(push_to_undo=push_to_undo)
446
+
447
+ def set_3d_features_enabled(self, enabled: bool) -> None:
448
+ """Enable or disable the 3D visualization panel and related UI actions.
449
+
450
+ Args:
451
+ enabled: True to enable 3D features, False to disable.
452
+ """
453
+ mw = self.get_main_window()
454
+ if mw and hasattr(mw, "ui_manager"):
455
+ enable = getattr(mw.ui_manager, "enable_3d_features", None)
456
+ if enable is not None:
457
+ enable(enabled)
458
+
459
+ def set_analysis_enabled(self, enabled: bool) -> None:
460
+ """Enable or disable the Analysis action in the main menu.
461
+
462
+ Args:
463
+ enabled: True to enable, False to disable.
464
+ """
465
+ mw = self.get_main_window()
466
+ if mw and hasattr(mw, "init_manager"):
467
+ action = getattr(mw.init_manager, "analysis_action", None)
468
+ if action is not None:
469
+ action.setEnabled(enabled)
470
+
471
+ def check_chemistry_problems(self) -> None:
472
+ """Trigger a chemistry validation pass and update problem flags on atoms."""
473
+ mw = self.get_main_window()
474
+ if mw and hasattr(mw, "compute_manager"):
475
+ check = getattr(
476
+ mw.compute_manager, "check_chemistry_problems_fallback", None
477
+ )
478
+ if check is not None:
479
+ check()
480
+
481
+ def refresh_2d_scene(self) -> None:
482
+ """Force a full redraw of the 2D canvas.
483
+
484
+ Recalculates ring geometry, then repaints every atom and bond item.
485
+ Use this after directly manipulating scene items (e.g. via scene.create_atom)
486
+ without going through context.current_molecule.
487
+ For a lightweight Qt repaint only, use context.scene.update() instead.
488
+ """
489
+ mw = self.get_main_window()
490
+ if mw and hasattr(mw, "init_manager"):
491
+ scene = getattr(mw.init_manager, "scene", None)
492
+ if scene is not None and hasattr(scene, "update_all_items"):
493
+ scene.update_all_items()
494
+
495
+ def load_from_smiles(self, smiles: str) -> None:
496
+ """Add a molecule from a SMILES string to the 2D editor."""
497
+ mw = self.get_main_window()
498
+ if mw and hasattr(mw, "string_importer_manager"):
499
+ mw.string_importer_manager.load_from_smiles(smiles)
500
+
501
+ def to_xyz_block(self) -> Optional[str]:
502
+ """Return the current 3D structure as an XYZ block (only element x y z lines)."""
503
+ mol = self.current_mol
504
+ if not mol:
505
+ return None
506
+
507
+ try:
508
+ conf = mol.GetConformer()
509
+ num_atoms = mol.GetNumAtoms()
510
+ xyz_lines = []
511
+
512
+ for i in range(num_atoms):
513
+ pos = conf.GetAtomPosition(i)
514
+ symbol = mol.GetAtomWithIdx(i).GetSymbol()
515
+ xyz_lines.append(
516
+ f" {symbol:<5}{pos.x:>15.8f}{pos.y:>15.8f}{pos.z:>15.8f}"
517
+ )
518
+
519
+ return "\n".join(xyz_lines)
520
+ except Exception:
521
+ return None
522
+
374
523
 
375
524
  class Plugin3DController:
376
525
  """Helper to manipulate the 3D scene."""
@@ -25,15 +25,11 @@ from PyQt6.QtCore import QUrl
25
25
  from PyQt6.QtGui import QDesktopServices
26
26
  from PyQt6.QtWidgets import QMessageBox
27
27
 
28
- try:
29
- from .plugin_interface import PluginContext
30
- except ImportError:
31
- # Fallback if running as script
32
- from moleditpy_linux.plugins.plugin_interface import PluginContext
28
+ from .plugin_interface import PluginContext
33
29
 
34
30
 
35
31
  class PluginManager:
36
- def _compute_sha256(self, path: str) -> str:
32
+ def compute_sha256(self, path: str) -> str:
37
33
  """Computes SHA-256 for a file or a directory (concatenated hashes of all files)."""
38
34
  if os.path.isfile(path):
39
35
  return self._sha256_for_file(path)
@@ -66,7 +62,10 @@ class PluginManager:
66
62
  hasher.update(rel_path.encode("utf-8", errors="replace"))
67
63
  hasher.update(b"\0")
68
64
  with open(file_path, "rb") as f:
69
- for chunk in iter(lambda: f.read(8192), b""):
65
+ while True:
66
+ chunk = f.read(8192)
67
+ if not chunk:
68
+ break
70
69
  hasher.update(chunk)
71
70
  hasher.update(b"\0")
72
71
  return hasher.hexdigest()
@@ -363,35 +362,18 @@ class PluginManager:
363
362
  # Pass category to context if needed, currently not storing it in context directly but could be useful
364
363
  try:
365
364
  module.initialize(context)
366
- except (
367
- AttributeError,
368
- RuntimeError,
369
- ValueError,
370
- OSError,
371
- ImportError,
372
- SyntaxError,
373
- ) as e:
374
- # [BROAD EXCEPTION] Plugins have root power; catch all potential failures during init.
365
+ except Exception as e: # plugins have full app access; catch everything to isolate faults
375
366
  status = f"Error (Init): {e}"
376
- # Initialization errors are stored in plugin status for display in Plugin Dialog
377
- logging.error(f"Plugin {plugin_name} initialize error: {e}")
367
+ logging.exception("Plugin %s initialize error", plugin_name)
378
368
  elif has_autorun:
379
369
  try:
380
370
  if self.main_window:
381
371
  module.autorun(self.main_window)
382
372
  else:
383
373
  status = "Skipped (No MW)"
384
- except (
385
- AttributeError,
386
- RuntimeError,
387
- ValueError,
388
- OSError,
389
- ImportError,
390
- SyntaxError,
391
- ) as e:
374
+ except Exception as e: # plugins have full app access; catch everything to isolate faults
392
375
  status = f"Error (Autorun): {e}"
393
- # Autorun errors are stored in plugin status
394
- print(f"Plugin {plugin_name} autorun error: {e}")
376
+ logging.exception("Plugin %s autorun error", plugin_name)
395
377
  elif not has_run:
396
378
  status = "No Entry Point"
397
379
 
@@ -409,36 +391,25 @@ class PluginManager:
409
391
  }
410
392
  )
411
393
 
412
- except (
413
- AttributeError,
414
- RuntimeError,
415
- ValueError,
416
- OSError,
417
- ImportError,
418
- SyntaxError,
419
- ) as e:
420
- # [BROAD EXCEPTION] Loading failures are caught to prevent a single buggy plugin from
421
- # crashing the entire discovery process.
422
- logging.error(f"Failed to load plugin {module_name}: {e}")
394
+ except Exception as e: # plugins have full app access; isolate any load failure to prevent crashing discovery
395
+ logging.exception("Failed to load plugin %s: %s", module_name, e)
423
396
 
424
397
  def run_plugin(self, module: Any, main_window: Any) -> None:
425
398
  """Executes the plugin's run method (Legacy manual trigger)."""
426
399
  try:
427
400
  module.run(main_window)
428
- except (
429
- AttributeError,
430
- RuntimeError,
431
- ValueError,
432
- OSError,
433
- ImportError,
434
- SyntaxError,
435
- ) as e:
401
+ except Exception as e: # plugins have full app access; catch everything so the error dialog can show the cause
436
402
  QMessageBox.critical(
437
403
  main_window,
438
404
  "Plugin Error",
439
405
  f"Error running plugin '{getattr(module, 'PLUGIN_NAME', 'Unknown')}':\n{e}",
440
406
  )
441
407
 
408
+ def rebuild_plugin_menus(self) -> None:
409
+ """Rebuild all plugin menus and toolbars."""
410
+ if self.main_window and hasattr(self.main_window, "plugin_menu_manager"):
411
+ self.main_window.plugin_menu_manager.rebuild_plugin_menus()
412
+
442
413
  # --- Registration Callbacks ---
443
414
  def register_menu_action(
444
415
  self,
@@ -614,9 +585,11 @@ class PluginManager:
614
585
  selected_indices.append(i)
615
586
  except (RuntimeError, ValueError, TypeError):
616
587
  continue
617
- except ImportError:
588
+ except (
589
+ ImportError
590
+ ): # [OPTIONAL DEP] importlib.metadata unavailable (<3.8); silently skip.
618
591
  pass
619
- except Exception as e:
592
+ except (RuntimeError, AttributeError) as e:
620
593
  logging.error(f"Error retrieving selected atom indices: {e}")
621
594
 
622
595
  return selected_indices
@@ -636,17 +609,9 @@ class PluginManager:
636
609
  for handler in self.document_reset_handlers:
637
610
  try:
638
611
  handler["callback"]()
639
- except (
640
- AttributeError,
641
- RuntimeError,
642
- ValueError,
643
- OSError,
644
- ImportError,
645
- SyntaxError,
646
- ) as e:
647
- # [BROAD EXCEPTION] Document reset handlers are user plugins; catch all to prevent data loss.
648
- logging.error(
649
- f"Error in document reset handler for {handler['plugin']}: {e}"
612
+ except Exception as e: # plugins have full app access; catch everything to prevent data loss on document reset
613
+ logging.exception(
614
+ "Error in document reset handler for %s: %s", handler["plugin"], e
650
615
  )
651
616
 
652
617
  def get_plugin_info_safe(self, file_path: str) -> Dict[str, str]:
@@ -675,10 +640,6 @@ class PluginManager:
675
640
  if node.value: # AnnAssign might presumably not have value? (though usually does for module globals)
676
641
  if isinstance(node.value, ast.Constant): # Py3.8+
677
642
  val = node.value.value
678
- elif hasattr(ast, "Str") and isinstance(
679
- node.value, getattr(ast, "Str", type(None))
680
- ): # Py3.7 and below
681
- val = node.value.s
682
643
  elif isinstance(node.value, ast.Tuple):
683
644
  # Handle version tuples e.g. (1, 0, 0)
684
645
  try:
@@ -687,18 +648,13 @@ class PluginManager:
687
648
  for elt in node.value.elts:
688
649
  if isinstance(elt, ast.Constant):
689
650
  elts.append(elt.value)
690
- elif hasattr(ast, "Num") and isinstance(
691
- elt, getattr(ast, "Num", type(None))
692
- ):
693
- elts.append(elt.n)
694
651
  val = ".".join(map(str, elts))
695
652
  except (
696
653
  AttributeError,
697
654
  RuntimeError,
698
655
  ValueError,
699
656
  TypeError,
700
- ):
701
- # Fallback for complex AST structures during metadata extraction
657
+ ): # [AST PARSE] Complex/unexpected AST node shapes during metadata extraction; skip gracefully.
702
658
  pass
703
659
 
704
660
  if val is not None: