MoleditPy 3.0.0a5__tar.gz → 3.0.0a7__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 (78) hide show
  1. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/PKG-INFO +4 -4
  2. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/README.md +3 -3
  3. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/pyproject.toml +1 -1
  4. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/MoleditPy.egg-info/PKG-INFO +4 -4
  5. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/core/mol_geometry.py +14 -5
  6. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/core/molecular_data.py +14 -4
  7. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/main.py +0 -1
  8. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/plugins/plugin_interface.py +85 -6
  9. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/plugins/plugin_manager.py +38 -11
  10. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/align_plane_dialog.py +1 -5
  11. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/alignment_dialog.py +1 -4
  12. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/angle_dialog.py +2 -4
  13. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/app_state.py +109 -36
  14. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/atom_item.py +24 -15
  15. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/base_picking_dialog.py +11 -4
  16. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/bond_item.py +71 -57
  17. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/bond_length_dialog.py +21 -26
  18. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/calculation_worker.py +2 -2
  19. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/color_settings_dialog.py +37 -18
  20. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/compute_logic.py +90 -42
  21. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/constrained_optimization_dialog.py +24 -7
  22. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/custom_interactor_style.py +69 -23
  23. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/custom_qt_interactor.py +2 -2
  24. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/dialog_3d_picking_mixin.py +20 -6
  25. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/dialog_logic.py +75 -21
  26. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/dihedral_dialog.py +31 -18
  27. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/edit_3d_logic.py +38 -13
  28. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/edit_actions_logic.py +111 -39
  29. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/export_logic.py +73 -23
  30. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/geometry_base_dialog.py +11 -9
  31. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/io_logic.py +335 -134
  32. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/main_window.py +12 -9
  33. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/main_window_init.py +291 -101
  34. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/molecular_scene_handler.py +44 -14
  35. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/molecule_scene.py +35 -14
  36. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/move_group_dialog.py +83 -27
  37. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/planarize_dialog.py +2 -4
  38. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/settings_dialog.py +16 -8
  39. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +2 -3
  40. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +15 -5
  41. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +3 -3
  42. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/string_importers.py +7 -4
  43. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/translation_dialog.py +5 -6
  44. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/ui_manager.py +84 -46
  45. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/user_template_dialog.py +22 -7
  46. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/view_3d_logic.py +224 -78
  47. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/utils/constants.py +1 -1
  48. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/utils/default_settings.py +0 -9
  49. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/LICENSE +0 -0
  50. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/setup.cfg +0 -0
  51. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  52. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  53. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  54. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/MoleditPy.egg-info/requires.txt +0 -0
  55. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/MoleditPy.egg-info/top_level.txt +0 -0
  56. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/__init__.py +0 -0
  57. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/__main__.py +0 -0
  58. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/assets/file_icon.ico +0 -0
  59. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/assets/icon.icns +0 -0
  60. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/assets/icon.ico +0 -0
  61. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/assets/icon.png +0 -0
  62. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/core/__init__.py +0 -0
  63. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/plugins/__init__.py +0 -0
  64. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/plugins/plugin_manager_window.py +0 -0
  65. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/__init__.py +0 -0
  66. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/about_dialog.py +0 -0
  67. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/analysis_window.py +0 -0
  68. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/mirror_dialog.py +0 -0
  69. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  70. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
  71. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
  72. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/sip_isdeleted_safe.py +0 -0
  73. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/template_preview_item.py +0 -0
  74. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/template_preview_view.py +0 -0
  75. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/ui/zoomable_view.py +0 -0
  76. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/utils/__init__.py +0 -0
  77. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
  78. {moleditpy-3.0.0a5 → moleditpy-3.0.0a7}/src/moleditpy/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.0.0a5
3
+ Version: 3.0.0a7
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
@@ -710,10 +710,10 @@ Dynamic: license-file
710
710
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
711
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
712
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
713
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-80%25-green)
714
- ![Overall Coverage](https://img.shields.io/badge/coverage-64%25-green)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-75%25-green)
714
+ ![Overall Coverage](https://img.shields.io/badge/coverage-67%25-green)
715
715
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
- ![Pylint Score](https://img.shields.io/badge/pylint-8.93%2F10-brightgreen)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint-9%2F10-brightgreen)
717
717
  [![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)
718
718
 
719
719
  [🇯🇵 日本語 (Japanese)](#japanese)
@@ -6,10 +6,10 @@
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-64%25-green)
9
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-75%25-green)
10
+ ![Overall Coverage](https://img.shields.io/badge/coverage-67%25-green)
11
11
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
12
- ![Pylint Score](https://img.shields.io/badge/pylint-8.93%2F10-brightgreen)
12
+ ![Pylint Score](https://img.shields.io/badge/pylint-9%2F10-brightgreen)
13
13
  [![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)
14
14
 
15
15
  [🇯🇵 日本語 (Japanese)](#japanese)
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "3.0.0a5"
8
+ version = "3.0.0a7"
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.0a5
3
+ Version: 3.0.0a7
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
@@ -710,10 +710,10 @@ Dynamic: license-file
710
710
  [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
711
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
712
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
713
- ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-80%25-green)
714
- ![Overall Coverage](https://img.shields.io/badge/coverage-64%25-green)
713
+ ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-75%25-green)
714
+ ![Overall Coverage](https://img.shields.io/badge/coverage-67%25-green)
715
715
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
- ![Pylint Score](https://img.shields.io/badge/pylint-8.93%2F10-brightgreen)
716
+ ![Pylint Score](https://img.shields.io/badge/pylint-9%2F10-brightgreen)
717
717
  [![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)
718
718
 
719
719
  [🇯🇵 日本語 (Japanese)](#japanese)
@@ -12,7 +12,6 @@ DOI: 10.5281/zenodo.17268532
12
12
 
13
13
  from __future__ import annotations
14
14
  import math
15
- import logging
16
15
  from collections import deque
17
16
  from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
18
17
 
@@ -22,7 +21,11 @@ import numpy as np
22
21
  # Primitive geometry helpers
23
22
  # ------------------------------------------------------------------
24
23
 
25
- def calc_distance(pos1: Union[np.ndarray, Tuple[float, float, float], List[float]], pos2: Union[np.ndarray, Tuple[float, float, float], List[float]]) -> float:
24
+
25
+ def calc_distance(
26
+ pos1: Union[np.ndarray, Tuple[float, float, float], List[float]],
27
+ pos2: Union[np.ndarray, Tuple[float, float, float], List[float]],
28
+ ) -> float:
26
29
  """Return the Euclidean distance between two 3-D positions.
27
30
 
28
31
  Parameters
@@ -78,7 +81,9 @@ def calc_angle_deg(
78
81
  # ------------------------------------------------------------------
79
82
 
80
83
 
81
- def get_connected_group(mol: Any, start_atom: int, exclude: Optional[int] = None) -> Set[int]:
84
+ def get_connected_group(
85
+ mol: Any, start_atom: int, exclude: Optional[int] = None
86
+ ) -> Set[int]:
82
87
  """Return the set of atom indices reachable from *start_atom*
83
88
  without passing through *exclude*.
84
89
 
@@ -383,7 +388,9 @@ _VALENCE_LIMITS = {
383
388
  }
384
389
 
385
390
 
386
- def is_problematic_valence(symbol: str, bond_count: Union[int, float], charge: int = 0) -> bool:
391
+ def is_problematic_valence(
392
+ symbol: str, bond_count: Union[int, float], charge: int = 0
393
+ ) -> bool:
387
394
  """Return ``True`` if the atom's total bond order exceeds its
388
395
  typical maximum valence.
389
396
 
@@ -475,7 +482,9 @@ def inject_ez_stereo_to_mol_block(mol_block, rdkit_mol, bonds_data):
475
482
  return "\n".join(mol_lines)
476
483
 
477
484
 
478
- def identify_valence_problems(atoms_data: Dict[int, Any], bonds_data: Dict[Tuple[int, int], Any]) -> List[int]:
485
+ def identify_valence_problems(
486
+ atoms_data: Dict[int, Any], bonds_data: Dict[Tuple[int, int], Any]
487
+ ) -> List[int]:
479
488
  """Identify atoms with problematic valence.
480
489
 
481
490
  Parameters
@@ -12,7 +12,7 @@ DOI: 10.5281/zenodo.17268532
12
12
 
13
13
  from __future__ import annotations
14
14
  import logging
15
- from typing import Any, Dict, List, Optional, Set, Tuple, Union
15
+ from typing import Any, Dict, List, Optional, Tuple, Union
16
16
  from rdkit import Chem
17
17
 
18
18
  try:
@@ -43,7 +43,13 @@ class MolecularData:
43
43
  self._next_atom_id = 0
44
44
  self.adjacency_list = {}
45
45
 
46
- def add_atom(self, symbol: str, pos: Union[Any, Tuple[float, float]], charge: int = 0, radical: int = 0) -> int:
46
+ def add_atom(
47
+ self,
48
+ symbol: str,
49
+ pos: Union[Any, Tuple[float, float]],
50
+ charge: int = 0,
51
+ radical: int = 0,
52
+ ) -> int:
47
53
  atom_id = self._next_atom_id
48
54
  # Internalize position as raw floats to decouple from UI types (QPointF)
49
55
  if hasattr(pos, "x") and hasattr(pos, "y"):
@@ -72,7 +78,9 @@ class MolecularData:
72
78
  else:
73
79
  self.atoms[atom_id]["pos"] = PointTuple((float(pos[0]), float(pos[1])))
74
80
 
75
- def add_bond(self, id1: int, id2: int, order: Union[int, float] = 1, stereo: int = 0) -> Tuple[Tuple[int, int], str]:
81
+ def add_bond(
82
+ self, id1: int, id2: int, order: Union[int, float] = 1, stereo: int = 0
83
+ ) -> Tuple[Tuple[int, int], str]:
76
84
  # For stereo bonds, do not sort because ID order determines direction.
77
85
  # For non-stereo bonds, sort to normalize the key.
78
86
  if stereo == 0:
@@ -457,7 +465,9 @@ class MolecularData:
457
465
  mol_block += "M END\n"
458
466
  return mol_block
459
467
 
460
- def to_template_dict(self, name: str, version: str = "1.0", application_version: str = "") -> Dict[str, Any]:
468
+ def to_template_dict(
469
+ self, name: str, version: str = "1.0", application_version: str = ""
470
+ ) -> Dict[str, Any]:
461
471
  """Convert current structure to a dictionary for template storage."""
462
472
  import datetime
463
473
 
@@ -50,7 +50,6 @@ def setup_logging():
50
50
  sys.excepthook = handle_exception
51
51
 
52
52
 
53
-
54
53
  def main():
55
54
  # Setup logging as early as possible
56
55
  setup_logging()
@@ -45,6 +45,50 @@ class PluginContext:
45
45
  self._plugin_name, path, callback, text, icon, shortcut
46
46
  )
47
47
 
48
+ def register_menu_action(
49
+ self,
50
+ path: str,
51
+ text_or_callback: Union[str, Callable],
52
+ callback: Optional[Callable] = None,
53
+ icon: Optional[str] = None,
54
+ shortcut: Optional[str] = None,
55
+ ):
56
+ """Backward-compatible alias for add_menu_action.
57
+ Supports old 3-arg style: register_menu_action(path, text, callback).
58
+ """
59
+ if callable(text_or_callback):
60
+ # New style: (path, callback, ...)
61
+ self.add_menu_action(path, text_or_callback, None, icon, shortcut)
62
+ else:
63
+ # Old style: (path, text, callback)
64
+ self.add_menu_action(path, callback, text_or_callback, icon, shortcut)
65
+
66
+ def add_plugin_menu(
67
+ self,
68
+ path: str,
69
+ callback: Callable,
70
+ text: Optional[str] = None,
71
+ icon: Optional[str] = None,
72
+ shortcut: Optional[str] = None,
73
+ ):
74
+ """
75
+ Register an action nested inside the Plugin menu.
76
+
77
+ Equivalent to add_menu_action("Plugin/<path>", ...).
78
+ Use this instead of add_menu_action when you want your plugin to appear
79
+ as a nested folder inside the Plugin menu rather than as a top-level menu.
80
+
81
+ Args:
82
+ path: Sub-path within the Plugin menu, e.g. "Utility/My Tool"
83
+ results in Plugin > Utility > My Tool.
84
+ callback: Function to call when triggered.
85
+ text: Label override (defaults to last part of path).
86
+ icon: Path to icon (optional).
87
+ shortcut: Keyboard shortcut (optional).
88
+ """
89
+ full_path = f"Plugin/{path.lstrip('/')}"
90
+ self.add_menu_action(full_path, callback, text, icon, shortcut)
91
+
48
92
  def add_toolbar_action(
49
93
  self,
50
94
  callback: Callable,
@@ -84,7 +128,7 @@ class PluginContext:
84
128
  def push_undo_checkpoint(self) -> None:
85
129
  """
86
130
  Create an undo checkpoint for the current state.
87
- Call this AFTER making modifications to the molecule to ensure the
131
+ Call this AFTER making modifications to the molecule to ensure the
88
132
  new state is saved to the undo history.
89
133
  """
90
134
  self._manager.push_undo_checkpoint()
@@ -135,13 +179,13 @@ class PluginContext:
135
179
  Get or set the current molecule (RDKit Mol object). Shortcut for current_molecule.
136
180
  """
137
181
  mw = self.get_main_window()
138
- return mw.current_mol if mw else None
182
+ return mw.view_3d_manager.current_mol if mw and hasattr(mw, "view_3d_manager") else None
139
183
 
140
184
  @current_mol.setter
141
185
  def current_mol(self, mol: Any):
142
186
  mw = self.get_main_window()
143
- if mw:
144
- mw.current_mol = mol
187
+ if mw and hasattr(mw, "view_3d_manager"):
188
+ mw.view_3d_manager.current_mol = mol
145
189
  if hasattr(mw.view_3d_manager, "draw_molecule_3d"):
146
190
  mw.view_3d_manager.draw_molecule_3d(mol)
147
191
 
@@ -160,7 +204,7 @@ class PluginContext:
160
204
  Returns the PyVista plotter from the MainWindow.
161
205
  """
162
206
  mw = self.get_main_window()
163
- return mw.plotter if mw else None
207
+ return mw.view_3d_manager.plotter if mw and hasattr(mw, "view_3d_manager") else None
164
208
 
165
209
  @property
166
210
  def scene(self) -> Any:
@@ -168,7 +212,7 @@ class PluginContext:
168
212
  Returns the 2D MoleculeScene from the MainWindow.
169
213
  """
170
214
  mw = self.get_main_window()
171
- return mw.scene if mw else None
215
+ return mw.init_manager.scene if mw and hasattr(mw, "init_manager") else None
172
216
 
173
217
  def add_export_action(self, label: str, callback: Callable):
174
218
  """
@@ -265,6 +309,41 @@ class PluginContext:
265
309
  """
266
310
  self._manager.register_document_reset_handler(self._plugin_name, callback)
267
311
 
312
+ def get_setting(self, key: str, default: Any = None) -> Any:
313
+ """
314
+ Get a plugin-specific persistent setting.
315
+
316
+ Settings are stored in the app settings dict under 'plugin.<plugin_name>.<key>'
317
+ and are persisted across sessions with the application settings.
318
+
319
+ Args:
320
+ key: Setting key name.
321
+ default: Value to return if the setting is not found.
322
+ """
323
+ mw = self.get_main_window()
324
+ if mw and hasattr(mw, "init_manager") and hasattr(mw.init_manager, "settings"):
325
+ namespaced = f"plugin.{self._plugin_name}.{key}"
326
+ return mw.init_manager.settings.get(namespaced, default)
327
+ return default
328
+
329
+ def set_setting(self, key: str, value: Any) -> None:
330
+ """
331
+ Save a plugin-specific persistent setting.
332
+
333
+ Settings are stored in the app settings dict under 'plugin.<plugin_name>.<key>'
334
+ and are saved when the application saves its settings.
335
+
336
+ Args:
337
+ key: Setting key name.
338
+ value: Value to store (must be JSON-serializable).
339
+ """
340
+ mw = self.get_main_window()
341
+ if mw and hasattr(mw, "init_manager") and hasattr(mw.init_manager, "settings"):
342
+ namespaced = f"plugin.{self._plugin_name}.{key}"
343
+ mw.init_manager.settings[namespaced] = value
344
+ if hasattr(mw.init_manager, "settings_dirty"):
345
+ mw.init_manager.settings_dirty = True
346
+
268
347
 
269
348
  class Plugin3DController:
270
349
  """Helper to manipulate the 3D scene."""
@@ -18,7 +18,7 @@ import os
18
18
  import shutil
19
19
  import sys
20
20
  import zipfile
21
- from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
21
+ from typing import Any, Callable, Dict, List, Optional, Tuple
22
22
 
23
23
  from PyQt6.QtCore import QUrl
24
24
  from PyQt6.QtGui import QDesktopServices
@@ -30,9 +30,12 @@ except ImportError:
30
30
  # Fallback if running as script
31
31
  from moleditpy.plugins.plugin_interface import PluginContext
32
32
 
33
+
33
34
  class PluginManager:
34
35
  def __init__(self, main_window: Any = None) -> None:
35
- self.plugin_dir: str = os.path.join(os.path.expanduser("~"), ".moleditpy", "plugins")
36
+ self.plugin_dir: str = os.path.join(
37
+ os.path.expanduser("~"), ".moleditpy", "plugins"
38
+ )
36
39
  self.plugins: List[Dict[str, Any]] = [] # List of dicts
37
40
  self.main_window: Any = main_window
38
41
 
@@ -50,7 +53,9 @@ class PluginManager:
50
53
  self.load_handlers: Dict[str, Callable] = {}
51
54
  self.custom_3d_styles: Dict[str, Dict[str, Any]] = {}
52
55
  self.document_reset_handlers: List[Dict[str, Any]] = []
53
- self.plugin_windows: Dict[str, Dict[str, Any]] = {} # Map of plugin_name -> {window_id -> window}
56
+ self.plugin_windows: Dict[
57
+ str, Dict[str, Any]
58
+ ] = {} # Map of plugin_name -> {window_id -> window}
54
59
 
55
60
  def get_main_window(self) -> Any:
56
61
  return self.main_window
@@ -235,7 +240,9 @@ class PluginManager:
235
240
 
236
241
  return self.plugins
237
242
 
238
- def _load_single_plugin(self, filepath: str, module_name: str, category: str) -> None:
243
+ def _load_single_plugin(
244
+ self, filepath: str, module_name: str, category: str
245
+ ) -> None:
239
246
  """Common loading logic for both single-file and package plugins."""
240
247
  try:
241
248
  # Ensure unique module name by including category path
@@ -371,7 +378,15 @@ class PluginManager:
371
378
  )
372
379
 
373
380
  # --- Registration Callbacks ---
374
- def register_menu_action(self, plugin_name: str, path: str, callback: Callable, text: str, icon: str, shortcut: str) -> None:
381
+ def register_menu_action(
382
+ self,
383
+ plugin_name: str,
384
+ path: str,
385
+ callback: Callable,
386
+ text: str,
387
+ icon: str,
388
+ shortcut: str,
389
+ ) -> None:
375
390
  self.menu_actions.append(
376
391
  {
377
392
  "plugin": plugin_name,
@@ -383,7 +398,9 @@ class PluginManager:
383
398
  }
384
399
  )
385
400
 
386
- def register_toolbar_action(self, plugin_name: str, callback: Callable, text: str, icon: str, tooltip: str) -> None:
401
+ def register_toolbar_action(
402
+ self, plugin_name: str, callback: Callable, text: str, icon: str, tooltip: str
403
+ ) -> None:
387
404
  self.toolbar_actions.append(
388
405
  {
389
406
  "plugin": plugin_name,
@@ -394,7 +411,9 @@ class PluginManager:
394
411
  }
395
412
  )
396
413
 
397
- def register_drop_handler(self, plugin_name: str, callback: Callable, priority: int) -> None:
414
+ def register_drop_handler(
415
+ self, plugin_name: str, callback: Callable, priority: int
416
+ ) -> None:
398
417
  self.drop_handlers.append(
399
418
  {"priority": priority, "plugin": plugin_name, "callback": callback}
400
419
  )
@@ -470,12 +489,20 @@ class PluginManager:
470
489
 
471
490
  def refresh_3d_view(self) -> None:
472
491
  """Force a re-render of the 3D scene."""
473
- if self.main_window and hasattr(self.main_window, "plotter") and self.main_window.plotter:
492
+ if (
493
+ self.main_window
494
+ and hasattr(self.main_window, "plotter")
495
+ and self.main_window.plotter
496
+ ):
474
497
  self.main_window.plotter.render()
475
498
 
476
499
  def reset_3d_camera(self) -> None:
477
500
  """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:
501
+ if (
502
+ self.main_window
503
+ and hasattr(self.main_window, "plotter")
504
+ and self.main_window.plotter
505
+ ):
479
506
  self.main_window.plotter.reset_camera()
480
507
  self.main_window.plotter.render()
481
508
 
@@ -492,7 +519,7 @@ class PluginManager:
492
519
  # In MoleditPy, atoms in the scene are AtomItem objects which have an 'atom_id'.
493
520
  # These atom_ids map to entries in state_manager.data.atoms.
494
521
  # RDKit molecule atoms have an '_original_atom_id' property.
495
-
522
+
496
523
  scene = getattr(self.main_window, "scene", None)
497
524
  if scene:
498
525
  selected_items = scene.selectedItems()
@@ -501,7 +528,7 @@ class PluginManager:
501
528
  # Relying on duck-typing for AtomItem
502
529
  if hasattr(item, "atom_id"):
503
530
  selected_atom_ids.add(item.atom_id)
504
-
531
+
505
532
  # Now map these editor IDs to RDKit indices
506
533
  mol = getattr(self.main_window, "current_mol", None)
507
534
  if mol and selected_atom_ids:
@@ -10,18 +10,14 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
- import logging
14
13
  import numpy as np
15
- from PyQt6.QtCore import Qt
16
14
  from PyQt6.QtWidgets import (
17
15
  QHBoxLayout,
18
16
  QLabel,
19
17
  QMessageBox,
20
18
  QPushButton,
21
19
  QVBoxLayout,
22
- QWidget,
23
20
  )
24
- from rdkit import Geometry
25
21
 
26
22
  try:
27
23
  from .base_picking_dialog import BasePickingDialog
@@ -122,7 +118,7 @@ class AlignPlaneDialog(BasePickingDialog):
122
118
  # fallback to main_window data map
123
119
  self.selected_atoms = (
124
120
  set(self.main_window.state_manager.data.atoms.keys())
125
- if hasattr(self.main_window.state_manager, 'data')
121
+ if hasattr(self.main_window.state_manager, "data")
126
122
  else set()
127
123
  )
128
124
 
@@ -10,7 +10,6 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
- import logging
14
13
  import numpy as np
15
14
  from PyQt6.QtWidgets import (
16
15
  QDialog,
@@ -128,9 +127,7 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
128
127
  elif len(self.selected_atoms) == 1:
129
128
  selected_list = list(self.selected_atoms)
130
129
  atom = self.mol.GetAtomWithIdx(selected_list[0])
131
- self.selection_label.setText(
132
- f"Selected 1 atom: {atom.GetSymbol()}"
133
- )
130
+ self.selection_label.setText(f"Selected 1 atom: {atom.GetSymbol()}")
134
131
  self.apply_button.setEnabled(False)
135
132
  elif len(self.selected_atoms) == 2:
136
133
  selected_list = sorted(list(self.selected_atoms))
@@ -10,7 +10,6 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
- import logging
14
13
  from PyQt6.QtWidgets import (
15
14
  QHBoxLayout,
16
15
  QLabel,
@@ -23,7 +22,6 @@ from PyQt6.QtWidgets import (
23
22
  QMessageBox,
24
23
  )
25
24
  from PyQt6.QtCore import Qt
26
- from rdkit import Geometry
27
25
 
28
26
  try:
29
27
  from .geometry_base_dialog import GeometryBaseDialog
@@ -91,7 +89,7 @@ class AngleDialog(GeometryBaseDialog):
91
89
  self.angle_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
92
90
  self.angle_slider.setTickInterval(45)
93
91
  self.angle_slider.setEnabled(False)
94
-
92
+
95
93
  # Connect to base class real-time handlers
96
94
  self.angle_slider.sliderPressed.connect(self.on_slider_pressed)
97
95
  self.angle_slider.sliderMoved.connect(
@@ -101,7 +99,7 @@ class AngleDialog(GeometryBaseDialog):
101
99
  self.angle_slider.valueChanged.connect(
102
100
  lambda v: self.on_slider_value_changed_click(v, self.angle_input, 1.0)
103
101
  )
104
-
102
+
105
103
  layout.addLayout(angle_layout)
106
104
  layout.addWidget(self.angle_slider)
107
105