MoleditPy 3.0.0a4__tar.gz → 3.0.0a5__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 (90) hide show
  1. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/PKG-INFO +1 -1
  2. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/pyproject.toml +1 -1
  3. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/SOURCES.txt +4 -6
  5. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/core/mol_geometry.py +64 -0
  6. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/core/molecular_data.py +1 -0
  7. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/plugins/plugin_interface.py +112 -21
  8. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/plugins/plugin_manager.py +77 -0
  9. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/plugins/plugin_manager_window.py +313 -313
  10. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/about_dialog.py +2 -2
  11. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/align_plane_dialog.py +20 -94
  12. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/alignment_dialog.py +8 -7
  13. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/angle_dialog.py +406 -516
  14. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/app_state.py +172 -283
  15. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/atom_item.py +6 -5
  16. moleditpy-3.0.0a5/src/moleditpy/ui/base_picking_dialog.py +119 -0
  17. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/bond_item.py +33 -61
  18. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/bond_length_dialog.py +356 -456
  19. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/calculation_worker.py +32 -18
  20. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/color_settings_dialog.py +66 -35
  21. moleditpy-3.0.0a5/src/moleditpy/ui/compute_logic.py +625 -0
  22. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/constrained_optimization_dialog.py +26 -26
  23. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/custom_interactor_style.py +83 -101
  24. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/custom_qt_interactor.py +4 -4
  25. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/dialog_3d_picking_mixin.py +39 -34
  26. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/dialog_logic.py +121 -135
  27. moleditpy-3.0.0a5/src/moleditpy/ui/dihedral_dialog.py +393 -0
  28. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/edit_3d_logic.py +47 -48
  29. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/edit_actions_logic.py +312 -153
  30. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/export_logic.py +106 -144
  31. moleditpy-3.0.0a5/src/moleditpy/ui/geometry_base_dialog.py +111 -0
  32. moleditpy-3.0.0a5/src/moleditpy/ui/io_logic.py +835 -0
  33. moleditpy-3.0.0a5/src/moleditpy/ui/main_window.py +97 -0
  34. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/main_window_init.py +507 -521
  35. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/mirror_dialog.py +3 -3
  36. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/molecular_scene_handler.py +43 -33
  37. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/molecule_scene.py +51 -19
  38. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/move_group_dialog.py +123 -259
  39. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/planarize_dialog.py +22 -65
  40. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_dialog.py +33 -66
  41. {moleditpy-3.0.0a4/src/moleditpy/utils → moleditpy-3.0.0a5/src/moleditpy/ui/settings_tabs}/__init__.py +11 -11
  42. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +6 -34
  43. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +20 -75
  44. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +18 -6
  45. moleditpy-3.0.0a5/src/moleditpy/ui/settings_tabs/settings_tab_base.py +63 -0
  46. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/string_importers.py +52 -45
  47. moleditpy-3.0.0a5/src/moleditpy/ui/translation_dialog.py +194 -0
  48. moleditpy-3.0.0a5/src/moleditpy/ui/ui_manager.py +567 -0
  49. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/user_template_dialog.py +30 -33
  50. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/view_3d_logic.py +271 -196
  51. {moleditpy-3.0.0a4/src/moleditpy/plugins → moleditpy-3.0.0a5/src/moleditpy/utils}/__init__.py +11 -11
  52. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/utils/constants.py +1 -1
  53. moleditpy-3.0.0a5/src/moleditpy/utils/default_settings.py +94 -0
  54. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/utils/sip_isdeleted_safe.py +41 -41
  55. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/utils/system_utils.py +71 -71
  56. moleditpy-3.0.0a4/src/moleditpy/modules/__init__.py +0 -122
  57. moleditpy-3.0.0a4/src/moleditpy/ui/compute_engine.py +0 -880
  58. moleditpy-3.0.0a4/src/moleditpy/ui/compute_logic.py +0 -529
  59. moleditpy-3.0.0a4/src/moleditpy/ui/dialog_manager.py +0 -464
  60. moleditpy-3.0.0a4/src/moleditpy/ui/dihedral_dialog.py +0 -545
  61. moleditpy-3.0.0a4/src/moleditpy/ui/main_window.py +0 -218
  62. moleditpy-3.0.0a4/src/moleditpy/ui/molecular_parsers.py +0 -592
  63. moleditpy-3.0.0a4/src/moleditpy/ui/project_io.py +0 -387
  64. moleditpy-3.0.0a4/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -33
  65. moleditpy-3.0.0a4/src/moleditpy/ui/translation_dialog.py +0 -347
  66. moleditpy-3.0.0a4/src/moleditpy/ui/ui_manager.py +0 -511
  67. moleditpy-3.0.0a4/src/moleditpy/ui/view_loaders.py +0 -274
  68. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/LICENSE +0 -0
  69. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/README.md +0 -0
  70. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/setup.cfg +0 -0
  71. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  72. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  73. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/requires.txt +0 -0
  74. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/MoleditPy.egg-info/top_level.txt +0 -0
  75. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/__init__.py +0 -0
  76. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/__main__.py +0 -0
  77. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/assets/file_icon.ico +0 -0
  78. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/assets/icon.icns +0 -0
  79. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/assets/icon.ico +0 -0
  80. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/assets/icon.png +0 -0
  81. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/core/__init__.py +0 -0
  82. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/main.py +0 -0
  83. {moleditpy-3.0.0a4/src/moleditpy/ui/settings_tabs → moleditpy-3.0.0a5/src/moleditpy/plugins}/__init__.py +0 -0
  84. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/__init__.py +0 -0
  85. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/analysis_window.py +0 -0
  86. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  87. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/sip_isdeleted_safe.py +0 -0
  88. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/template_preview_item.py +0 -0
  89. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/template_preview_view.py +0 -0
  90. {moleditpy-3.0.0a4 → moleditpy-3.0.0a5}/src/moleditpy/ui/zoomable_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.0.0a4
3
+ Version: 3.0.0a5
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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "3.0.0a4"
8
+ version = "3.0.0a5"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.0.0a4
3
+ Version: 3.0.0a5
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
@@ -23,7 +23,6 @@ src/moleditpy/assets/icon.png
23
23
  src/moleditpy/core/__init__.py
24
24
  src/moleditpy/core/mol_geometry.py
25
25
  src/moleditpy/core/molecular_data.py
26
- src/moleditpy/modules/__init__.py
27
26
  src/moleditpy/plugins/__init__.py
28
27
  src/moleditpy/plugins/plugin_interface.py
29
28
  src/moleditpy/plugins/plugin_manager.py
@@ -36,32 +35,31 @@ src/moleditpy/ui/analysis_window.py
36
35
  src/moleditpy/ui/angle_dialog.py
37
36
  src/moleditpy/ui/app_state.py
38
37
  src/moleditpy/ui/atom_item.py
38
+ src/moleditpy/ui/base_picking_dialog.py
39
39
  src/moleditpy/ui/bond_item.py
40
40
  src/moleditpy/ui/bond_length_dialog.py
41
41
  src/moleditpy/ui/calculation_worker.py
42
42
  src/moleditpy/ui/color_settings_dialog.py
43
- src/moleditpy/ui/compute_engine.py
44
43
  src/moleditpy/ui/compute_logic.py
45
44
  src/moleditpy/ui/constrained_optimization_dialog.py
46
45
  src/moleditpy/ui/custom_interactor_style.py
47
46
  src/moleditpy/ui/custom_qt_interactor.py
48
47
  src/moleditpy/ui/dialog_3d_picking_mixin.py
49
48
  src/moleditpy/ui/dialog_logic.py
50
- src/moleditpy/ui/dialog_manager.py
51
49
  src/moleditpy/ui/dihedral_dialog.py
52
50
  src/moleditpy/ui/edit_3d_logic.py
53
51
  src/moleditpy/ui/edit_actions_logic.py
54
52
  src/moleditpy/ui/export_logic.py
53
+ src/moleditpy/ui/geometry_base_dialog.py
54
+ src/moleditpy/ui/io_logic.py
55
55
  src/moleditpy/ui/main_window.py
56
56
  src/moleditpy/ui/main_window_init.py
57
57
  src/moleditpy/ui/mirror_dialog.py
58
- src/moleditpy/ui/molecular_parsers.py
59
58
  src/moleditpy/ui/molecular_scene_handler.py
60
59
  src/moleditpy/ui/molecule_scene.py
61
60
  src/moleditpy/ui/move_group_dialog.py
62
61
  src/moleditpy/ui/periodic_table_dialog.py
63
62
  src/moleditpy/ui/planarize_dialog.py
64
- src/moleditpy/ui/project_io.py
65
63
  src/moleditpy/ui/settings_dialog.py
66
64
  src/moleditpy/ui/sip_isdeleted_safe.py
67
65
  src/moleditpy/ui/string_importers.py
@@ -71,7 +69,6 @@ src/moleditpy/ui/translation_dialog.py
71
69
  src/moleditpy/ui/ui_manager.py
72
70
  src/moleditpy/ui/user_template_dialog.py
73
71
  src/moleditpy/ui/view_3d_logic.py
74
- src/moleditpy/ui/view_loaders.py
75
72
  src/moleditpy/ui/zoomable_view.py
76
73
  src/moleditpy/ui/settings_tabs/__init__.py
77
74
  src/moleditpy/ui/settings_tabs/settings_2d_tab.py
@@ -80,5 +77,6 @@ src/moleditpy/ui/settings_tabs/settings_other_tab.py
80
77
  src/moleditpy/ui/settings_tabs/settings_tab_base.py
81
78
  src/moleditpy/utils/__init__.py
82
79
  src/moleditpy/utils/constants.py
80
+ src/moleditpy/utils/default_settings.py
83
81
  src/moleditpy/utils/sip_isdeleted_safe.py
84
82
  src/moleditpy/utils/system_utils.py
@@ -300,6 +300,70 @@ def calculate_dihedral(positions: Any, i1: int, i2: int, i3: int, i4: int) -> fl
300
300
  return float(np.degrees(angle_rad))
301
301
 
302
302
 
303
+ def adjust_dihedral(
304
+ positions: np.ndarray,
305
+ i1: int,
306
+ i2: int,
307
+ i3: int,
308
+ i4: int,
309
+ target_dihedral_deg: float,
310
+ atom_indices_to_move: Iterable[int],
311
+ ) -> float:
312
+ """Adjust the dihedral angle defined by i1-i2-i3-i4 to target_dihedral_deg.
313
+ The rotation is performed around the i2-i3 bond axis.
314
+
315
+ Parameters
316
+ ----------
317
+ positions : ndarray, shape (N, 3)
318
+ Atom coordinates. **Modified in-place.**
319
+ i1, i2, i3, i4 : int
320
+ Atom indices defining the dihedral.
321
+ target_dihedral_deg : float
322
+ Target dihedral angle in degrees.
323
+ atom_indices_to_move : iterable of int
324
+ Indices of atoms to be rotated.
325
+
326
+ Returns
327
+ -------
328
+ float
329
+ Applied rotation in radians.
330
+ """
331
+ # Current dihedral
332
+ current_dihedral = calculate_dihedral(positions, i1, i2, i3, i4)
333
+
334
+ # Rotation angle needed
335
+ delta_deg = target_dihedral_deg - current_dihedral
336
+
337
+ # Shortest rotation path
338
+ if delta_deg > 180:
339
+ delta_deg -= 360
340
+ elif delta_deg < -180:
341
+ delta_deg += 360
342
+
343
+ delta_rad = np.radians(delta_deg)
344
+
345
+ if abs(delta_rad) < 1e-9:
346
+ return 0.0
347
+
348
+ # Rotation axis (i2 -> i3)
349
+ pos2 = positions[i2]
350
+ pos3 = positions[i3]
351
+ axis = pos3 - pos2
352
+ axis_norm = np.linalg.norm(axis)
353
+
354
+ if axis_norm < 1e-12:
355
+ return 0.0
356
+
357
+ axis_unit = axis / axis_norm
358
+
359
+ # Rotate each movable atom
360
+ for idx in atom_indices_to_move:
361
+ rel = positions[idx] - pos2
362
+ positions[idx] = pos2 + rodrigues_rotate(rel, axis_unit, delta_rad)
363
+
364
+ return float(delta_rad)
365
+
366
+
303
367
  # ------------------------------------------------------------------
304
368
  # Valence sanity check
305
369
  # ------------------------------------------------------------------
@@ -154,6 +154,7 @@ class MolecularData:
154
154
  atom.SetFormalCharge(data.get("charge", 0))
155
155
  atom.SetNumRadicalElectrons(data.get("radical", 0))
156
156
  atom.SetIntProp("_original_atom_id", atom_id)
157
+ atom.SetNoImplicit(False) # Allow RDKit to perceive implicit valence
157
158
  idx = mol.AddAtom(atom)
158
159
  atom_id_to_idx_map[atom_id] = idx
159
160
 
@@ -10,7 +10,7 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
- from typing import Any, Callable, Optional
13
+ from typing import Any, Callable, List, Optional, Union
14
14
 
15
15
 
16
16
  class PluginContext:
@@ -75,6 +75,53 @@ class PluginContext:
75
75
  """
76
76
  return Plugin3DController(self._manager.get_main_window())
77
77
 
78
+ def show_status_message(self, message: str, timeout: int = 3000) -> None:
79
+ """
80
+ Display a message in the application status bar.
81
+ """
82
+ self._manager.show_status_message(message, timeout)
83
+
84
+ def push_undo_checkpoint(self) -> None:
85
+ """
86
+ Create an undo checkpoint for the current state.
87
+ Call this AFTER making modifications to the molecule to ensure the
88
+ new state is saved to the undo history.
89
+ """
90
+ self._manager.push_undo_checkpoint()
91
+
92
+ def refresh_3d_view(self) -> None:
93
+ """
94
+ Force a refresh (re-render) of the 3D scene.
95
+ """
96
+ self._manager.refresh_3d_view()
97
+
98
+ def reset_3d_camera(self) -> None:
99
+ """
100
+ Resets the 3D camera to fit the current molecule.
101
+ """
102
+ self._manager.reset_3d_camera()
103
+
104
+ def get_selected_atom_indices(self) -> List[int]:
105
+ """
106
+ Returns a list of RDKit atom indices currently selected in the 2D or 3D view.
107
+ Note: RDKit indices are returned, which map to the current_mol.
108
+ """
109
+ return self._manager.get_selected_atom_indices()
110
+
111
+ def register_window(self, window_id: str, window: Any) -> None:
112
+ """
113
+ Register a custom plugin window/dialog with the application.
114
+ This allows the application to manage the window lifecycle.
115
+ Windows are namespaced by the plugin name automatically.
116
+ """
117
+ self._manager.register_window(self._plugin_name, window_id, window)
118
+
119
+ def get_window(self, window_id: str) -> Optional[Any]:
120
+ """
121
+ Retrieve a previously registered window by its ID.
122
+ """
123
+ return self._manager.get_window(self._plugin_name, window_id)
124
+
78
125
  def get_main_window(self) -> Any:
79
126
  """
80
127
  Returns the raw MainWindow instance.
@@ -83,22 +130,45 @@ class PluginContext:
83
130
  return self._manager.get_main_window()
84
131
 
85
132
  @property
86
- def current_molecule(self) -> Any:
133
+ def current_mol(self) -> Any:
87
134
  """
88
- Get or set the current molecule (RDKit Mol object).
135
+ Get or set the current molecule (RDKit Mol object). Shortcut for current_molecule.
89
136
  """
90
- mw = self._manager.get_main_window()
137
+ mw = self.get_main_window()
138
+ return mw.current_mol if mw else None
139
+
140
+ @current_mol.setter
141
+ def current_mol(self, mol: Any):
142
+ mw = self.get_main_window()
91
143
  if mw:
92
- return mw.current_mol
93
- return None
144
+ mw.current_mol = mol
145
+ if hasattr(mw.view_3d_manager, "draw_molecule_3d"):
146
+ mw.view_3d_manager.draw_molecule_3d(mol)
147
+
148
+ @property
149
+ def current_molecule(self) -> Any:
150
+ """Alias for current_mol for backward compatibility."""
151
+ return self.current_mol
94
152
 
95
153
  @current_molecule.setter
96
154
  def current_molecule(self, mol: Any):
97
- mw = self._manager.get_main_window()
98
- if mw:
99
- mw.current_mol = mol
100
- if hasattr(mw, "draw_molecule_3d"):
101
- mw.draw_molecule_3d(mol)
155
+ self.current_mol = mol
156
+
157
+ @property
158
+ def plotter(self) -> Any:
159
+ """
160
+ Returns the PyVista plotter from the MainWindow.
161
+ """
162
+ mw = self.get_main_window()
163
+ return mw.plotter if mw else None
164
+
165
+ @property
166
+ def scene(self) -> Any:
167
+ """
168
+ Returns the 2D MoleculeScene from the MainWindow.
169
+ """
170
+ mw = self.get_main_window()
171
+ return mw.scene if mw else None
102
172
 
103
173
  def add_export_action(self, label: str, callback: Callable):
104
174
  """
@@ -202,6 +272,10 @@ class Plugin3DController:
202
272
  def __init__(self, main_window):
203
273
  self._mw = main_window
204
274
 
275
+ def _get_v3d(self):
276
+ """Helper to get the 3D manager."""
277
+ return getattr(self._mw, "view_3d_manager", None)
278
+
205
279
  def set_atom_color(self, atom_index: int, color_hex: str):
206
280
  """
207
281
  Set the color of a specific atom in the 3D view.
@@ -209,11 +283,11 @@ class Plugin3DController:
209
283
  atom_index: RDKit atom index.
210
284
  color_hex: Hex string e.g., "#FF0000".
211
285
  """
212
- if hasattr(self._mw, "main_window_view_3d"):
213
- self._mw.main_window_view_3d.update_atom_color_override(
214
- atom_index, color_hex
215
- )
216
- self._mw.plotter.render()
286
+ v3d = self._get_v3d()
287
+ if v3d:
288
+ v3d.update_atom_color_override(atom_index, color_hex)
289
+ if hasattr(self._mw, "plotter") and self._mw.plotter:
290
+ self._mw.plotter.render()
217
291
 
218
292
  def set_bond_color(self, bond_index: int, color_hex: str):
219
293
  """
@@ -223,8 +297,25 @@ class Plugin3DController:
223
297
  bond_index: RDKit bond index.
224
298
  color_hex: Hex string e.g., "#00FF00".
225
299
  """
226
- if hasattr(self._mw, "main_window_view_3d"):
227
- self._mw.main_window_view_3d.update_bond_color_override(
228
- bond_index, color_hex
229
- )
230
- self._mw.plotter.render()
300
+ v3d = self._get_v3d()
301
+ if v3d:
302
+ v3d.update_bond_color_override(bond_index, color_hex)
303
+ if hasattr(self._mw, "plotter") and self._mw.plotter:
304
+ self._mw.plotter.render()
305
+
306
+ def set_bond_color_by_atoms(self, atom_idx1: int, atom_idx2: int, color_hex: str):
307
+ """
308
+ Set the color of the bond between two atoms.
309
+
310
+ Args:
311
+ atom_idx1: First RDKit atom index.
312
+ atom_idx2: Second RDKit atom index.
313
+ color_hex: Hex string e.g., "#00FF00".
314
+ """
315
+ mol = getattr(self._mw, "current_mol", None)
316
+ if not mol:
317
+ return
318
+
319
+ bond = mol.GetBondBetweenAtoms(atom_idx1, atom_idx2)
320
+ if bond:
321
+ self.set_bond_color(bond.GetIdx(), color_hex)
@@ -50,6 +50,7 @@ class PluginManager:
50
50
  self.load_handlers: Dict[str, Callable] = {}
51
51
  self.custom_3d_styles: Dict[str, Dict[str, Any]] = {}
52
52
  self.document_reset_handlers: List[Dict[str, Any]] = []
53
+ self.plugin_windows: Dict[str, Dict[str, Any]] = {} # Map of plugin_name -> {window_id -> window}
53
54
 
54
55
  def get_main_window(self) -> Any:
55
56
  return self.main_window
@@ -454,6 +455,82 @@ class PluginManager:
454
455
  {"plugin": plugin_name, "callback": callback}
455
456
  )
456
457
 
458
+ # --- New API Implementation ---
459
+ def show_status_message(self, message: str, timeout: int = 3000) -> None:
460
+ """Display a message in the MainWindow status bar."""
461
+ if self.main_window and hasattr(self.main_window, "statusBar"):
462
+ status_bar = self.main_window.statusBar()
463
+ if status_bar:
464
+ status_bar.showMessage(message, timeout)
465
+
466
+ def push_undo_checkpoint(self) -> None:
467
+ """Triggers an undo checkpoint in the application state."""
468
+ if self.main_window and hasattr(self.main_window, "state_manager"):
469
+ self.main_window.state_manager.push_undo_state()
470
+
471
+ def refresh_3d_view(self) -> None:
472
+ """Force a re-render of the 3D scene."""
473
+ if self.main_window and hasattr(self.main_window, "plotter") and self.main_window.plotter:
474
+ self.main_window.plotter.render()
475
+
476
+ def reset_3d_camera(self) -> None:
477
+ """Reset the 3D camera to fit the current molecule."""
478
+ if self.main_window and hasattr(self.main_window, "plotter") and self.main_window.plotter:
479
+ self.main_window.plotter.reset_camera()
480
+ self.main_window.plotter.render()
481
+
482
+ def get_selected_atom_indices(self) -> List[int]:
483
+ """Retrieve RDKit atom indices currently selected in the 2D scene."""
484
+ selected_indices = []
485
+ if not self.main_window:
486
+ return []
487
+
488
+ # Check 2D selection
489
+ try:
490
+ from .plugin_interface import PluginContext
491
+ # We need to access the scene items.
492
+ # In MoleditPy, atoms in the scene are AtomItem objects which have an 'atom_id'.
493
+ # These atom_ids map to entries in state_manager.data.atoms.
494
+ # RDKit molecule atoms have an '_original_atom_id' property.
495
+
496
+ scene = getattr(self.main_window, "scene", None)
497
+ if scene:
498
+ selected_items = scene.selectedItems()
499
+ selected_atom_ids = set()
500
+ for item in selected_items:
501
+ # Relying on duck-typing for AtomItem
502
+ if hasattr(item, "atom_id"):
503
+ selected_atom_ids.add(item.atom_id)
504
+
505
+ # Now map these editor IDs to RDKit indices
506
+ mol = getattr(self.main_window, "current_mol", None)
507
+ if mol and selected_atom_ids:
508
+ for i in range(mol.GetNumAtoms()):
509
+ atom = mol.GetAtomWithIdx(i)
510
+ if atom.HasProp("_original_atom_id"):
511
+ try:
512
+ orig_id = atom.GetIntProp("_original_atom_id")
513
+ if orig_id in selected_atom_ids:
514
+ selected_indices.append(i)
515
+ except (RuntimeError, ValueError, TypeError):
516
+ continue
517
+ except ImportError:
518
+ pass
519
+ except Exception as e:
520
+ logging.error(f"Error retrieving selected atom indices: {e}")
521
+
522
+ return selected_indices
523
+
524
+ def register_window(self, plugin_name: str, window_id: str, window: Any) -> None:
525
+ """Register a plugin window to keep it alive and manageable."""
526
+ if plugin_name not in self.plugin_windows:
527
+ self.plugin_windows[plugin_name] = {}
528
+ self.plugin_windows[plugin_name][window_id] = window
529
+
530
+ def get_window(self, plugin_name: str, window_id: str) -> Optional[Any]:
531
+ """Retrieve a registered plugin window."""
532
+ return self.plugin_windows.get(plugin_name, {}).get(window_id)
533
+
457
534
  def invoke_document_reset_handlers(self) -> None:
458
535
  """Call all registered document reset handlers."""
459
536
  for handler in self.document_reset_handlers: