MoleditPy-linux 4.8.0__tar.gz → 4.8.2__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.
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/PKG-INFO +4 -4
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/README.md +3 -3
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/pyproject.toml +1 -1
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/MoleditPy_linux.egg-info/PKG-INFO +4 -4
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/plugins/plugin_interface.py +12 -2
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/plugins/plugin_manager.py +14 -2
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/calculation_worker.py +87 -10
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/edit_actions_logic.py +4 -2
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/molecular_scene_handler.py +121 -18
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/plugin_menu_manager.py +231 -60
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/template_preview_item.py +17 -1
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/LICENSE +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/setup.cfg +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/core/mol_geometry.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/core/molecular_data.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/main.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/about_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/analysis_window.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/app_state.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/atom_item.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/atom_picking.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/bond_item.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/chain_mixin.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/compute_logic.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/export_logic.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/io_logic.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/main_window.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/main_window_init.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/preview_molecule.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/string_importers.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/ui_manager.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/utils/constants.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/utils/default_settings.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/utils/hit_radius.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/utils/suppress_log.py +0 -0
- {moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/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.8.
|
|
3
|
+
Version: 4.8.2
|
|
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,9 +709,9 @@ Dynamic: license-file
|
|
|
709
709
|
[](https://pypi.org/project/MoleditPy/)
|
|
710
710
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
711
711
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
712
|
-

|
|
713
|
-

|
|
714
|
-

|
|
712
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/blob/main/tests/coverage_report.md)
|
|
713
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/blob/main/tests/MANUAL_CHECKLIST.md)
|
|
714
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/blob/main/tests/pylint-score.txt)
|
|
715
715
|
[](https://pepy.tech/projects/moleditpy)
|
|
716
716
|
[](https://pepy.tech/projects/moleditpy)
|
|
717
717
|
[](https://deepwiki.com/HiroYokoyama/python_molecular_editor)
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
[](https://pypi.org/project/MoleditPy/)
|
|
7
7
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
8
8
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
9
|
-

|
|
10
|
-

|
|
11
|
-

|
|
9
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/blob/main/tests/coverage_report.md)
|
|
10
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/blob/main/tests/MANUAL_CHECKLIST.md)
|
|
11
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/blob/main/tests/pylint-score.txt)
|
|
12
12
|
[](https://pepy.tech/projects/moleditpy)
|
|
13
13
|
[](https://pepy.tech/projects/moleditpy)
|
|
14
14
|
[](https://deepwiki.com/HiroYokoyama/python_molecular_editor)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 4.8.
|
|
3
|
+
Version: 4.8.2
|
|
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,9 +709,9 @@ Dynamic: license-file
|
|
|
709
709
|
[](https://pypi.org/project/MoleditPy/)
|
|
710
710
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
711
711
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
712
|
-

|
|
713
|
-

|
|
714
|
-

|
|
712
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/blob/main/tests/coverage_report.md)
|
|
713
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/blob/main/tests/MANUAL_CHECKLIST.md)
|
|
714
|
+
[](https://github.com/HiroYokoyama/python_molecular_editor/blob/main/tests/pylint-score.txt)
|
|
715
715
|
[](https://pepy.tech/projects/moleditpy)
|
|
716
716
|
[](https://pepy.tech/projects/moleditpy)
|
|
717
717
|
[](https://deepwiki.com/HiroYokoyama/python_molecular_editor)
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
@@ -31,6 +31,7 @@ class PluginContext:
|
|
|
31
31
|
text: Optional[str] = None,
|
|
32
32
|
icon: Optional[str] = None,
|
|
33
33
|
shortcut: Optional[str] = None,
|
|
34
|
+
pin: Optional[str] = None,
|
|
34
35
|
) -> None:
|
|
35
36
|
"""
|
|
36
37
|
Register a menu action.
|
|
@@ -41,9 +42,16 @@ class PluginContext:
|
|
|
41
42
|
text: Label for the action (defaults to last part of path if None).
|
|
42
43
|
icon: Path to icon or icon name (optional).
|
|
43
44
|
shortcut: Keyboard shortcut (optional).
|
|
45
|
+
pin: Placement request. ``"header"`` puts a direct "Plugin/<entry>"
|
|
46
|
+
registration next to "Plugin Manager..." above the first
|
|
47
|
+
divider, for plugins that manage the plugin system itself.
|
|
48
|
+
Anything else is ignored, so unknown values stay forward
|
|
49
|
+
compatible. Added in 4.8.1 — passing it to an older MoleditPy
|
|
50
|
+
raises TypeError, so guard the call if your plugin supports
|
|
51
|
+
both (see PLUGIN_DEVELOPMENT_MANUAL_V4.md).
|
|
44
52
|
"""
|
|
45
53
|
self._manager.register_menu_action(
|
|
46
|
-
self._plugin_name, path, callback, text, icon, shortcut
|
|
54
|
+
self._plugin_name, path, callback, text, icon, shortcut, pin
|
|
47
55
|
)
|
|
48
56
|
|
|
49
57
|
def register_menu_action(
|
|
@@ -72,6 +80,7 @@ class PluginContext:
|
|
|
72
80
|
text: Optional[str] = None,
|
|
73
81
|
icon: Optional[str] = None,
|
|
74
82
|
shortcut: Optional[str] = None,
|
|
83
|
+
pin: Optional[str] = None,
|
|
75
84
|
) -> None:
|
|
76
85
|
"""
|
|
77
86
|
Register an action nested inside the Plugin menu.
|
|
@@ -87,9 +96,10 @@ class PluginContext:
|
|
|
87
96
|
text: Label override (defaults to last part of path).
|
|
88
97
|
icon: Path to icon (optional).
|
|
89
98
|
shortcut: Keyboard shortcut (optional).
|
|
99
|
+
pin: Placement request; see add_menu_action.
|
|
90
100
|
"""
|
|
91
101
|
full_path = f"Plugin/{path.lstrip('/')}"
|
|
92
|
-
self.add_menu_action(full_path, callback, text, icon, shortcut)
|
|
102
|
+
self.add_menu_action(full_path, callback, text, icon, shortcut, pin)
|
|
93
103
|
|
|
94
104
|
def add_toolbar_action(
|
|
95
105
|
self,
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
@@ -358,7 +358,13 @@ class PluginManager:
|
|
|
358
358
|
plugin_desc = getattr(
|
|
359
359
|
module, "PLUGIN_DESCRIPTION", getattr(module, "__doc__", "")
|
|
360
360
|
)
|
|
361
|
-
|
|
361
|
+
# The folder the user filed the plugin under wins: that
|
|
362
|
+
# placement is a deliberate, user-controlled ordering. A
|
|
363
|
+
# plugin's own PLUGIN_CATEGORY only applies when the file sits
|
|
364
|
+
# at the plugins root, where there is no folder to honour.
|
|
365
|
+
plugin_category = category or str(
|
|
366
|
+
getattr(module, "PLUGIN_CATEGORY", "") or ""
|
|
367
|
+
)
|
|
362
368
|
|
|
363
369
|
# Additional cleanup for docstring (strip whitespace)
|
|
364
370
|
if plugin_desc is None:
|
|
@@ -445,8 +451,13 @@ class PluginManager:
|
|
|
445
451
|
text: str,
|
|
446
452
|
icon: str,
|
|
447
453
|
shortcut: str,
|
|
454
|
+
pin: Optional[str] = None,
|
|
448
455
|
) -> None:
|
|
449
|
-
"""Register a plugin menu action with its path, callback, and display metadata.
|
|
456
|
+
"""Register a plugin menu action with its path, callback, and display metadata.
|
|
457
|
+
|
|
458
|
+
``pin`` is optional so a V3-era caller reaching this method directly
|
|
459
|
+
keeps working.
|
|
460
|
+
"""
|
|
450
461
|
self.menu_actions.append(
|
|
451
462
|
{
|
|
452
463
|
"plugin": plugin_name,
|
|
@@ -455,6 +466,7 @@ class PluginManager:
|
|
|
455
466
|
"text": text,
|
|
456
467
|
"icon": icon,
|
|
457
468
|
"shortcut": shortcut,
|
|
469
|
+
"pin": pin,
|
|
458
470
|
}
|
|
459
471
|
)
|
|
460
472
|
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/calculation_worker.py
RENAMED
|
@@ -26,6 +26,8 @@ from rdkit import Chem
|
|
|
26
26
|
from rdkit.Chem import AllChem, rdGeometry
|
|
27
27
|
from rdkit.DistanceGeometry import DoTriangleSmoothing
|
|
28
28
|
|
|
29
|
+
from ..utils.constants import DEFAULT_BOND_LENGTH_ANGSTROM
|
|
30
|
+
|
|
29
31
|
from .. import OBABEL_AVAILABLE
|
|
30
32
|
|
|
31
33
|
# Only import pybel on demand
|
|
@@ -367,6 +369,50 @@ def _apply_explicit_stereo(
|
|
|
367
369
|
bond.SetStereo(stereo_type)
|
|
368
370
|
|
|
369
371
|
|
|
372
|
+
# Typical X-H distances used when placing hydrogens the 2D sketch never had.
|
|
373
|
+
_DIRECT_XH_LENGTHS = {6: 1.09, 7: 1.01, 8: 0.97, 15: 1.42, 16: 1.34}
|
|
374
|
+
_DIRECT_XH_DEFAULT = 1.10
|
|
375
|
+
# Out-of-plane tilt applied to added H and to wedge/hash bonds.
|
|
376
|
+
_DIRECT_H_TILT = math.radians(20.0)
|
|
377
|
+
_DIRECT_WEDGE_TILT = math.radians(35.0)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _rescale_2d_layout(
|
|
381
|
+
coords: list, mol: Chem.Mol, _safe_status: Callable[[str], None]
|
|
382
|
+
) -> list:
|
|
383
|
+
"""Scale parsed 2D coordinates so the median bond is a chemically sane length."""
|
|
384
|
+
lengths = []
|
|
385
|
+
for bond in mol.GetBonds():
|
|
386
|
+
i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
|
|
387
|
+
if i < len(coords) and j < len(coords):
|
|
388
|
+
d = math.hypot(coords[i][0] - coords[j][0], coords[i][1] - coords[j][1])
|
|
389
|
+
if d > 1e-6:
|
|
390
|
+
lengths.append(d)
|
|
391
|
+
if not lengths:
|
|
392
|
+
return coords
|
|
393
|
+
|
|
394
|
+
median = float(np.median(lengths))
|
|
395
|
+
if median < 1e-6 or abs(median - DEFAULT_BOND_LENGTH_ANGSTROM) < 0.05:
|
|
396
|
+
return coords
|
|
397
|
+
|
|
398
|
+
scale = DEFAULT_BOND_LENGTH_ANGSTROM / median
|
|
399
|
+
_safe_status(f"Rescaling 2D layout (x{scale:.3f})...")
|
|
400
|
+
return [(x * scale, y * scale, z * scale) for x, y, z in coords]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _center_conformer(conf: Chem.Conformer) -> None:
|
|
404
|
+
"""Move the conformer's centroid to the origin."""
|
|
405
|
+
n = conf.GetNumAtoms()
|
|
406
|
+
if n == 0:
|
|
407
|
+
return
|
|
408
|
+
pos = np.array(
|
|
409
|
+
[[p.x, p.y, p.z] for p in (conf.GetAtomPosition(i) for i in range(n))],
|
|
410
|
+
dtype=float,
|
|
411
|
+
)
|
|
412
|
+
for i, (x, y, z) in enumerate(pos - pos.mean(axis=0)):
|
|
413
|
+
conf.SetAtomPosition(i, rdGeometry.Point3D(float(x), float(y), float(z)))
|
|
414
|
+
|
|
415
|
+
|
|
370
416
|
def _perform_direct_conversion(
|
|
371
417
|
mol_block: str,
|
|
372
418
|
mol: Chem.Mol,
|
|
@@ -446,6 +492,8 @@ def _perform_direct_conversion(
|
|
|
446
492
|
if not parsed_coords:
|
|
447
493
|
raise ValueError("Failed to parse coordinates for direct conversion.")
|
|
448
494
|
|
|
495
|
+
parsed_coords = _rescale_2d_layout(parsed_coords, mol, _safe_status)
|
|
496
|
+
|
|
449
497
|
num_existing = len(parsed_coords)
|
|
450
498
|
conf = Chem.Conformer(mol.GetNumAtoms())
|
|
451
499
|
for i in range(mol.GetNumAtoms()):
|
|
@@ -480,28 +528,45 @@ def _perform_direct_conversion(
|
|
|
480
528
|
vecs[0][0],
|
|
481
529
|
math.hypot(-vecs[0][1], vecs[0][0]),
|
|
482
530
|
)
|
|
483
|
-
|
|
531
|
+
n_placed = sum(
|
|
484
532
|
1
|
|
485
533
|
for n in mol.GetAtomWithIdx(parent_idx).GetNeighbors()
|
|
486
534
|
if n.GetIdx() < i and n.GetAtomicNum() == 1
|
|
487
|
-
)
|
|
535
|
+
)
|
|
536
|
+
angle = n_placed * (math.pi / 6.0)
|
|
488
537
|
rx, ry = (
|
|
489
538
|
(fx / fn) * math.cos(angle) - (fy / fn) * math.sin(angle),
|
|
490
539
|
(fx / fn) * math.sin(angle) + (fy / fn) * math.cos(angle),
|
|
491
540
|
)
|
|
541
|
+
bond_len = _DIRECT_XH_LENGTHS.get(
|
|
542
|
+
mol.GetAtomWithIdx(parent_idx).GetAtomicNum(),
|
|
543
|
+
_DIRECT_XH_DEFAULT,
|
|
544
|
+
)
|
|
545
|
+
in_plane = bond_len * math.cos(_DIRECT_H_TILT)
|
|
546
|
+
# Alternate the pucker so a CH2's two hydrogens straddle
|
|
547
|
+
# the plane instead of stacking on the same side.
|
|
548
|
+
dz = bond_len * math.sin(_DIRECT_H_TILT)
|
|
549
|
+
if n_placed % 2:
|
|
550
|
+
dz = -dz
|
|
492
551
|
conf.SetAtomPosition(
|
|
493
552
|
i,
|
|
494
553
|
rdGeometry.Point3D(
|
|
495
|
-
float(parent_pos.x) + rx
|
|
554
|
+
float(parent_pos.x) + rx * in_plane,
|
|
555
|
+
float(parent_pos.y) + ry * in_plane,
|
|
556
|
+
float(parent_pos.z) + dz,
|
|
496
557
|
),
|
|
497
558
|
)
|
|
498
559
|
else:
|
|
560
|
+
bond_len = _DIRECT_XH_LENGTHS.get(
|
|
561
|
+
mol.GetAtomWithIdx(parent_idx).GetAtomicNum(),
|
|
562
|
+
_DIRECT_XH_DEFAULT,
|
|
563
|
+
)
|
|
499
564
|
conf.SetAtomPosition(
|
|
500
565
|
i,
|
|
501
566
|
rdGeometry.Point3D(
|
|
502
|
-
float(parent_pos.x) +
|
|
503
|
-
float(parent_pos.y)
|
|
504
|
-
|
|
567
|
+
float(parent_pos.x) + bond_len,
|
|
568
|
+
float(parent_pos.y),
|
|
569
|
+
float(parent_pos.z),
|
|
505
570
|
),
|
|
506
571
|
)
|
|
507
572
|
else:
|
|
@@ -511,16 +576,28 @@ def _perform_direct_conversion(
|
|
|
511
576
|
with suppress_log(AttributeError, RuntimeError, TypeError, IndexError):
|
|
512
577
|
for b, e, flag in stereo_dirs:
|
|
513
578
|
if b < num_existing and e < num_existing:
|
|
514
|
-
pos = conf.GetAtomPosition(e)
|
|
579
|
+
start, pos = conf.GetAtomPosition(b), conf.GetAtomPosition(e)
|
|
580
|
+
vx, vy = float(pos.x) - float(start.x), float(pos.y) - float(start.y)
|
|
581
|
+
length = math.hypot(vx, vy)
|
|
582
|
+
if length < 1e-6:
|
|
583
|
+
continue
|
|
584
|
+
# Rotate the bond out of plane so it keeps its length; adding a
|
|
585
|
+
# flat +/-1.5 A of z stretched every wedge bond instead.
|
|
586
|
+
sign = 1.0 if flag == 1 else -1.0
|
|
587
|
+
scale = math.cos(_DIRECT_WEDGE_TILT)
|
|
515
588
|
conf.SetAtomPosition(
|
|
516
589
|
e,
|
|
517
590
|
rdGeometry.Point3D(
|
|
518
|
-
float(
|
|
519
|
-
float(
|
|
520
|
-
float(
|
|
591
|
+
float(start.x) + vx * scale,
|
|
592
|
+
float(start.y) + vy * scale,
|
|
593
|
+
float(start.z) + sign * length * math.sin(_DIRECT_WEDGE_TILT),
|
|
521
594
|
),
|
|
522
595
|
)
|
|
523
596
|
|
|
597
|
+
# The sketch sits wherever it was drawn on the canvas, which can be hundreds
|
|
598
|
+
# of angstroms from the origin; centre it before handing it to the 3D view.
|
|
599
|
+
_center_conformer(conf)
|
|
600
|
+
|
|
524
601
|
mol.RemoveAllConformers()
|
|
525
602
|
mol.AddConformer(conf, assignId=True)
|
|
526
603
|
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/edit_actions_logic.py
RENAMED
|
@@ -883,9 +883,11 @@ class EditActionsManager:
|
|
|
883
883
|
self.host.set_current_file_path(None)
|
|
884
884
|
self.host.state_manager.update_window_title()
|
|
885
885
|
|
|
886
|
-
# Reset 2D zoom
|
|
886
|
+
# Reset 2D zoom to the launch default (75%), not the bare identity
|
|
887
|
+
# transform -- resetTransform() left the view at 100%, so Clear All
|
|
888
|
+
# came back zoomed in compared to a freshly started session.
|
|
887
889
|
if self.host.init_manager.view_2d:
|
|
888
|
-
self.host.
|
|
890
|
+
self.host.view_3d_manager.reset_zoom()
|
|
889
891
|
|
|
890
892
|
# Update scene and view
|
|
891
893
|
self.host.init_manager.scene.update()
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/molecular_scene_handler.py
RENAMED
|
@@ -24,6 +24,7 @@ from .bond_item import BondItem
|
|
|
24
24
|
|
|
25
25
|
from ..utils.sip_isdeleted_safe import sip_isdeleted_safe
|
|
26
26
|
|
|
27
|
+
from ..core.mol_geometry import is_problematic_valence
|
|
27
28
|
from ..utils.constants import DEFAULT_BOND_LENGTH, SUM_TOLERANCE
|
|
28
29
|
|
|
29
30
|
|
|
@@ -197,6 +198,106 @@ class TemplateMixin:
|
|
|
197
198
|
|
|
198
199
|
return not atom1_has_other_double and not atom2_has_other_double
|
|
199
200
|
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _bond_load(atom: Optional[AtomItem]) -> float:
|
|
203
|
+
"""Total bond order already attached to *atom*."""
|
|
204
|
+
if atom is None:
|
|
205
|
+
return 0.0
|
|
206
|
+
return sum(b.order for b in atom.bonds)
|
|
207
|
+
|
|
208
|
+
def _plan_template_bonds(
|
|
209
|
+
self,
|
|
210
|
+
bonds: List[Tuple[int, int, int]],
|
|
211
|
+
vertex_atoms: List[Optional[AtomItem]],
|
|
212
|
+
) -> Tuple[List[Tuple[int, int, int]], List[Tuple[int, int]]]:
|
|
213
|
+
"""Decide the order every template bond ends up with once placed.
|
|
214
|
+
|
|
215
|
+
Two things constrain the template's own alternation:
|
|
216
|
+
|
|
217
|
+
* a bond that already exists is kept, unless the benzene policy allows
|
|
218
|
+
re-ordering it *and* both atoms have room for the extra order;
|
|
219
|
+
* a Kekule ring always carries a double next to its fused bond, so
|
|
220
|
+
fusing onto a single bond whose atoms each already hold a double --
|
|
221
|
+
every peripheral single bond of naphthalene -- would hand one of them
|
|
222
|
+
a fifth bond. Those bonds are lowered instead, which leaves the new
|
|
223
|
+
carbons as methylene: a real molecule rather than a hypervalent one.
|
|
224
|
+
|
|
225
|
+
Existing bonds are settled first so that an allowed upgrade is in the
|
|
226
|
+
atom's load before the new bonds hanging off it are fitted, and loads
|
|
227
|
+
are carried across the ring so several new bonds on one atom count
|
|
228
|
+
together.
|
|
229
|
+
|
|
230
|
+
Returns the planned orders and the vertex pairs the editor already
|
|
231
|
+
draws unchanged -- the ghost skips those, since painting a second copy
|
|
232
|
+
over a real double bond reads as a triple.
|
|
233
|
+
"""
|
|
234
|
+
is_benzene_template = len(bonds) == 6 and any(o == 2 for _, _, o in bonds)
|
|
235
|
+
loads = {i: self._bond_load(atom) for i, atom in enumerate(vertex_atoms)}
|
|
236
|
+
|
|
237
|
+
def vertex(index: int) -> Optional[AtomItem]:
|
|
238
|
+
return vertex_atoms[index] if index < len(vertex_atoms) else None
|
|
239
|
+
|
|
240
|
+
def fits(index: int, extra: float) -> bool:
|
|
241
|
+
atom = vertex(index)
|
|
242
|
+
if atom is None or extra <= 0:
|
|
243
|
+
return True
|
|
244
|
+
return not is_problematic_valence(
|
|
245
|
+
getattr(atom, "symbol", "C"),
|
|
246
|
+
loads.get(index, 0.0) + extra,
|
|
247
|
+
int(getattr(atom, "charge", 0) or 0),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
planned: Dict[int, int] = {}
|
|
251
|
+
unchanged: List[Tuple[int, int]] = []
|
|
252
|
+
|
|
253
|
+
# Every new bond needs at least a single, so an upgrade has to leave
|
|
254
|
+
# room for the ones still to come as well as for itself
|
|
255
|
+
incoming: Dict[int, int] = {}
|
|
256
|
+
for i, j, _order in bonds:
|
|
257
|
+
atom_i, atom_j = vertex(i), vertex(j)
|
|
258
|
+
if atom_i and atom_j and self.find_bond_between(atom_i, atom_j):
|
|
259
|
+
continue
|
|
260
|
+
incoming[i] = incoming.get(i, 0) + 1
|
|
261
|
+
incoming[j] = incoming.get(j, 0) + 1
|
|
262
|
+
|
|
263
|
+
# Pass 1 -- bonds that are already drawn
|
|
264
|
+
for position, (i, j, order) in enumerate(bonds):
|
|
265
|
+
atom_i, atom_j = vertex(i), vertex(j)
|
|
266
|
+
exist_b = (
|
|
267
|
+
self.find_bond_between(atom_i, atom_j) if atom_i and atom_j else None
|
|
268
|
+
)
|
|
269
|
+
if exist_b is None:
|
|
270
|
+
continue
|
|
271
|
+
current = int(getattr(exist_b, "order", order))
|
|
272
|
+
extra = order - current
|
|
273
|
+
if (
|
|
274
|
+
is_benzene_template
|
|
275
|
+
and self._should_overwrite_benzene_bond(exist_b)
|
|
276
|
+
and fits(i, extra + incoming.get(i, 0))
|
|
277
|
+
and fits(j, extra + incoming.get(j, 0))
|
|
278
|
+
):
|
|
279
|
+
loads[i] = loads.get(i, 0.0) + extra
|
|
280
|
+
loads[j] = loads.get(j, 0.0) + extra
|
|
281
|
+
planned[position] = order
|
|
282
|
+
else:
|
|
283
|
+
planned[position] = current
|
|
284
|
+
unchanged.append((i, j))
|
|
285
|
+
|
|
286
|
+
# Pass 2 -- bonds the placement will create
|
|
287
|
+
for position, (i, j, order) in enumerate(bonds):
|
|
288
|
+
if position in planned:
|
|
289
|
+
continue
|
|
290
|
+
while order > 1 and not (fits(i, order) and fits(j, order)):
|
|
291
|
+
order -= 1
|
|
292
|
+
loads[i] = loads.get(i, 0.0) + order
|
|
293
|
+
loads[j] = loads.get(j, 0.0) + order
|
|
294
|
+
planned[position] = order
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
[(i, j, planned[position]) for position, (i, j, _) in enumerate(bonds)],
|
|
298
|
+
unchanged,
|
|
299
|
+
)
|
|
300
|
+
|
|
200
301
|
def add_molecule_fragment(
|
|
201
302
|
self,
|
|
202
303
|
points: List[Union[QPointF, Tuple[float, float]]],
|
|
@@ -214,8 +315,6 @@ class TemplateMixin:
|
|
|
214
315
|
num_points = len(points)
|
|
215
316
|
atom_items: List[Optional[AtomItem]] = [None] * num_points
|
|
216
317
|
|
|
217
|
-
is_benzene_template = num_points == 6 and any(o == 2 for _, _, o in bonds_info)
|
|
218
|
-
|
|
219
318
|
def coords(p: Any) -> Any:
|
|
220
319
|
if hasattr(p, "x") and hasattr(p, "y"):
|
|
221
320
|
return (p.x(), p.y())
|
|
@@ -317,6 +416,12 @@ class TemplateMixin:
|
|
|
317
416
|
new_tb.append((i_idx, j_idx, new_order))
|
|
318
417
|
template_bonds_to_use = new_tb
|
|
319
418
|
|
|
419
|
+
# Atoms exist but carry no template bonds yet, so their loads are the
|
|
420
|
+
# ones the fusion has to fit into.
|
|
421
|
+
template_bonds_to_use, _unchanged = self._plan_template_bonds(
|
|
422
|
+
list(template_bonds_to_use), atom_items
|
|
423
|
+
)
|
|
424
|
+
|
|
320
425
|
# --- 5) Bond Creation/Update ---
|
|
321
426
|
for id1_idx, id2_idx, order in template_bonds_to_use:
|
|
322
427
|
if id1_idx < len(atom_items) and id2_idx < len(atom_items):
|
|
@@ -331,22 +436,15 @@ class TemplateMixin:
|
|
|
331
436
|
exist_b = self.find_bond_between(a_item, b_item)
|
|
332
437
|
|
|
333
438
|
if exist_b:
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
and self._should_overwrite_benzene_bond(exist_b)
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
if should_overwrite:
|
|
341
|
-
# Update bond order if conditions met
|
|
342
|
-
exist_b.order = order
|
|
343
|
-
exist_b.stereo = 0
|
|
344
|
-
self.data.bonds[(id1, id2)]["order"] = order
|
|
345
|
-
self.data.bonds[(id1, id2)]["stereo"] = 0
|
|
346
|
-
exist_b.update()
|
|
347
|
-
else:
|
|
348
|
-
# Keep existing bond if conditions not met
|
|
439
|
+
# _plan_template_bonds already applied the keep-or-overwrite
|
|
440
|
+
# policy, so an unchanged order means keep this bond
|
|
441
|
+
if order == exist_b.order:
|
|
349
442
|
continue
|
|
443
|
+
exist_b.order = order
|
|
444
|
+
exist_b.stereo = 0
|
|
445
|
+
self.data.bonds[(id1, id2)]["order"] = order
|
|
446
|
+
self.data.bonds[(id1, id2)]["stereo"] = 0
|
|
447
|
+
exist_b.update()
|
|
350
448
|
else:
|
|
351
449
|
# Create new bond
|
|
352
450
|
self.create_bond(a_item, b_item, bond_order=order, bond_stereo=0)
|
|
@@ -514,7 +612,12 @@ class TemplateMixin:
|
|
|
514
612
|
for m, (i, j, _) in enumerate(bonds_info)
|
|
515
613
|
]
|
|
516
614
|
|
|
517
|
-
self.
|
|
615
|
+
preview_bonds, editor_drawn = self._plan_template_bonds(
|
|
616
|
+
preview_bonds, vertex_atoms
|
|
617
|
+
)
|
|
618
|
+
self.template_preview.set_geometry(
|
|
619
|
+
points, is_aromatic, preview_bonds, editor_drawn
|
|
620
|
+
)
|
|
518
621
|
|
|
519
622
|
self.template_preview.show()
|
|
520
623
|
if self.views():
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/plugin_menu_manager.py
RENAMED
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
|
-
from typing import Any, Callable, Dict, List, TYPE_CHECKING
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
|
|
18
18
|
|
|
19
19
|
from PyQt6.QtGui import QAction, QIcon, QKeySequence
|
|
20
20
|
from PyQt6.QtWidgets import QFileDialog, QMenu, QMessageBox
|
|
@@ -38,8 +38,18 @@ class PluginMenuManager:
|
|
|
38
38
|
# so the rebuild pass can find and remove exactly those on the next reload.
|
|
39
39
|
_PLUGIN_ACTION_TAG = "plugin_managed"
|
|
40
40
|
|
|
41
|
+
# Title of the menu that hosts "Plugin/<path>" registrations.
|
|
42
|
+
_PLUGIN_MENU_TITLE = "Plugin"
|
|
43
|
+
|
|
44
|
+
# Value of add_menu_action(pin=...) requesting the header slot: placed with
|
|
45
|
+
# "Plugin Manager..." above the first divider, for plugins that manage the
|
|
46
|
+
# plugin system itself rather than acting on a molecule.
|
|
47
|
+
_PIN_HEADER = "header"
|
|
48
|
+
|
|
41
49
|
def __init__(self, init_manager: MainInitManager) -> None:
|
|
42
50
|
self._im = init_manager
|
|
51
|
+
# Built once and re-added on every reset; see _reset_plugin_menu.
|
|
52
|
+
self._manage_action: Optional[QAction] = None
|
|
43
53
|
|
|
44
54
|
def _make_safe_callback(self, callback: Callable, plugin_name: str) -> Callable:
|
|
45
55
|
"""Wrap a plugin callback so exceptions don't propagate into Qt's signal machinery."""
|
|
@@ -99,12 +109,8 @@ class PluginMenuManager:
|
|
|
99
109
|
if not self._im.host.plugin_manager:
|
|
100
110
|
return
|
|
101
111
|
|
|
102
|
-
self._clear_all_plugin_actions(
|
|
103
|
-
|
|
104
|
-
manage_action = QAction("Plugin Manager...", self._im.host)
|
|
105
|
-
manage_action.triggered.connect(lambda: self._show_plugin_manager(plugin_menu))
|
|
106
|
-
plugin_menu.addAction(manage_action)
|
|
107
|
-
plugin_menu.addSeparator()
|
|
112
|
+
self._clear_all_plugin_actions()
|
|
113
|
+
self._reset_plugin_menu(plugin_menu)
|
|
108
114
|
|
|
109
115
|
plugins = self._im.host.plugin_manager.discover_plugins(self._im.host)
|
|
110
116
|
|
|
@@ -117,6 +123,33 @@ class PluginMenuManager:
|
|
|
117
123
|
self.integrate_plugin_analysis_tools()
|
|
118
124
|
self.integrate_plugin_optimization_methods()
|
|
119
125
|
|
|
126
|
+
def _reset_plugin_menu(self, plugin_menu: QMenu) -> None:
|
|
127
|
+
"""Empty the Plugin menu and re-add its fixed header.
|
|
128
|
+
|
|
129
|
+
Menu entries are parented to the host, not to the menu, so ``clear()``
|
|
130
|
+
only detaches them — without retiring the outgoing ones every rebuild
|
|
131
|
+
strands another set on the main window for the life of the process.
|
|
132
|
+
|
|
133
|
+
The manager entry is kept and re-added rather than rebuilt: a rebuild
|
|
134
|
+
can start from inside its own ``triggered`` handler (Plugin Manager ▸
|
|
135
|
+
Reload), and that handler is still on the stack, so this is the one
|
|
136
|
+
action that must not be deleted here.
|
|
137
|
+
"""
|
|
138
|
+
manage_action = self._manage_action
|
|
139
|
+
self._retire_menu_tree(plugin_menu, manage_action)
|
|
140
|
+
plugin_menu.clear()
|
|
141
|
+
|
|
142
|
+
if manage_action is None:
|
|
143
|
+
# Bound to the menu it was first built for, which in the app is
|
|
144
|
+
# always MainInitManager.plugin_menu.
|
|
145
|
+
manage_action = QAction("Plugin Manager...", self._im.host)
|
|
146
|
+
manage_action.triggered.connect(
|
|
147
|
+
lambda: self._show_plugin_manager(plugin_menu)
|
|
148
|
+
)
|
|
149
|
+
self._manage_action = manage_action
|
|
150
|
+
plugin_menu.addAction(manage_action)
|
|
151
|
+
plugin_menu.addSeparator()
|
|
152
|
+
|
|
120
153
|
def rebuild_plugin_menus(self) -> None:
|
|
121
154
|
"""Fully rebuild all plugin-managed UI after an install/uninstall.
|
|
122
155
|
|
|
@@ -124,43 +157,35 @@ class PluginMenuManager:
|
|
|
124
157
|
``PluginManager.discover_plugins`` has already been called and the
|
|
125
158
|
registries are fresh. It cleans the existing tagged actions and
|
|
126
159
|
re-populates every integration point: menus, toolbars, export,
|
|
127
|
-
file-openers, analysis tools, and 3D styles.
|
|
128
|
-
"""
|
|
160
|
+
file-openers, analysis tools, legacy run() entries, and 3D styles.
|
|
129
161
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if not any(not a.isSeparator() for a in submenu.actions()):
|
|
136
|
-
menu.removeAction(action)
|
|
137
|
-
elif action.data() == self._PLUGIN_ACTION_TAG:
|
|
138
|
-
self._remove_action(menu, action)
|
|
162
|
+
The Plugin menu is emptied and rebuilt rather than topped up, so a
|
|
163
|
+
reload lands the context-injected entries above the folder-derived ones
|
|
164
|
+
exactly as a fresh launch does. Appending to the surviving (untagged)
|
|
165
|
+
folder entries instead used to flip the two groups on every reload.
|
|
166
|
+
"""
|
|
139
167
|
|
|
140
168
|
try:
|
|
141
169
|
for menu in self._get_plugin_target_menus():
|
|
142
|
-
|
|
170
|
+
self._strip_plugin_actions(menu)
|
|
143
171
|
except Exception:
|
|
144
172
|
logging.warning("Plugin rebuild: menu cleanup error", exc_info=True)
|
|
145
173
|
|
|
146
|
-
|
|
147
|
-
try:
|
|
148
|
-
menu_bar = self._im.host.menuBar()
|
|
149
|
-
for top_action in list(menu_bar.actions()):
|
|
150
|
-
if top_action.data() != self._PLUGIN_ACTION_TAG:
|
|
151
|
-
continue
|
|
152
|
-
submenu = top_action.menu()
|
|
153
|
-
if submenu is None or not any(
|
|
154
|
-
not a.isSeparator() for a in submenu.actions()
|
|
155
|
-
):
|
|
156
|
-
menu_bar.removeAction(top_action)
|
|
157
|
-
except Exception:
|
|
158
|
-
logging.warning("Plugin rebuild: menubar cleanup error", exc_info=True)
|
|
174
|
+
self._clear_plugin_menubar_entries()
|
|
159
175
|
|
|
160
|
-
self._im
|
|
176
|
+
plugin_menu = getattr(self._im, "plugin_menu", None)
|
|
177
|
+
if plugin_menu is not None:
|
|
178
|
+
self._reset_plugin_menu(plugin_menu)
|
|
179
|
+
|
|
180
|
+
def _rebuild_legacy_actions() -> None:
|
|
181
|
+
if plugin_menu is not None:
|
|
182
|
+
self._add_legacy_plugin_actions(
|
|
183
|
+
plugin_menu, self._im.host.plugin_manager.plugins
|
|
184
|
+
)
|
|
161
185
|
|
|
162
186
|
for method, label in [
|
|
163
187
|
(self.add_registered_plugin_actions, "menu actions"),
|
|
188
|
+
(_rebuild_legacy_actions, "legacy plugin actions"),
|
|
164
189
|
(self.add_plugin_toolbar_actions, "toolbar actions"),
|
|
165
190
|
(self.integrate_plugin_export_actions, "export actions"),
|
|
166
191
|
(self.integrate_plugin_file_openers, "file openers"),
|
|
@@ -173,6 +198,129 @@ class PluginMenuManager:
|
|
|
173
198
|
except Exception:
|
|
174
199
|
logging.warning("Plugin rebuild: %s error", label, exc_info=True)
|
|
175
200
|
|
|
201
|
+
def _retire_menu_tree(self, menu: QMenu, keep: Optional[QAction]) -> None:
|
|
202
|
+
"""Destroy a menu subtree that ``clear()`` would only detach.
|
|
203
|
+
|
|
204
|
+
``clear()`` deletes the actions a menu owns, but plugin entries are
|
|
205
|
+
parented to the host and submenus are child widgets of the menu, so
|
|
206
|
+
both outlive it — a submenu's menuAction going away does not take the
|
|
207
|
+
QMenu with it. Retiring the tree explicitly keeps a reload from
|
|
208
|
+
stranding another copy of every folder submenu and its entries.
|
|
209
|
+
|
|
210
|
+
*keep* is spared: it is the manager entry, which may be the handler
|
|
211
|
+
the current rebuild was started from.
|
|
212
|
+
"""
|
|
213
|
+
for action in menu.actions():
|
|
214
|
+
submenu = action.menu()
|
|
215
|
+
if submenu is not None:
|
|
216
|
+
self._retire_menu_tree(submenu, keep)
|
|
217
|
+
submenu.deleteLater()
|
|
218
|
+
elif action is not keep:
|
|
219
|
+
action.deleteLater()
|
|
220
|
+
|
|
221
|
+
def _strip_plugin_actions(self, menu: QMenu) -> None:
|
|
222
|
+
"""Remove every tagged action from *menu* and its submenus.
|
|
223
|
+
|
|
224
|
+
Recurses before removing, so a plugin-created submenu is emptied
|
|
225
|
+
first and only then dropped — and destroyed with it. Detaching a
|
|
226
|
+
submenu without destroying it leaves the QMenu alive as a child of
|
|
227
|
+
its former parent, and the next populate pass, no longer finding it
|
|
228
|
+
among the parent's actions, builds another: one stranded menu per
|
|
229
|
+
rebuild for the life of the process.
|
|
230
|
+
|
|
231
|
+
Native submenus that happen to be empty are not ours to remove, hence
|
|
232
|
+
the tag check.
|
|
233
|
+
"""
|
|
234
|
+
for action in list(menu.actions()):
|
|
235
|
+
submenu = action.menu()
|
|
236
|
+
if submenu is not None:
|
|
237
|
+
self._strip_plugin_actions(submenu)
|
|
238
|
+
if action.data() == self._PLUGIN_ACTION_TAG and not any(
|
|
239
|
+
not a.isSeparator() for a in submenu.actions()
|
|
240
|
+
):
|
|
241
|
+
menu.removeAction(action)
|
|
242
|
+
submenu.deleteLater()
|
|
243
|
+
elif action.data() == self._PLUGIN_ACTION_TAG:
|
|
244
|
+
self._remove_action(menu, action)
|
|
245
|
+
|
|
246
|
+
def _clear_plugin_menubar_entries(self) -> None:
|
|
247
|
+
"""Drop emptied plugin-created top-level menus and the menubar divider.
|
|
248
|
+
|
|
249
|
+
Both clear paths need this. ``_get_plugin_target_menus`` walks *into*
|
|
250
|
+
the menus on the bar but never the bar itself, so without this pass an
|
|
251
|
+
uninstalled plugin's own top-level menu survives as an empty stub and
|
|
252
|
+
the divider in front of it is never reclaimed. Resetting the flag lets
|
|
253
|
+
the following populate pass re-add that divider.
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
menu_bar = self._im.host.menuBar()
|
|
257
|
+
for top_action in list(menu_bar.actions()):
|
|
258
|
+
if top_action.data() != self._PLUGIN_ACTION_TAG:
|
|
259
|
+
continue
|
|
260
|
+
submenu = top_action.menu()
|
|
261
|
+
if submenu is None or not any(
|
|
262
|
+
not a.isSeparator() for a in submenu.actions()
|
|
263
|
+
):
|
|
264
|
+
menu_bar.removeAction(top_action)
|
|
265
|
+
except Exception:
|
|
266
|
+
logging.warning("Plugin menubar cleanup error", exc_info=True)
|
|
267
|
+
|
|
268
|
+
self._im.plugin_menubar_separator_added = False
|
|
269
|
+
|
|
270
|
+
def _ensure_plugin_separator(self, menu: QMenu) -> None:
|
|
271
|
+
"""Add a tagged divider before the first plugin entry appended to *menu*.
|
|
272
|
+
|
|
273
|
+
The divider must land where the plugin content actually starts. For a
|
|
274
|
+
nested path like ``Tools/Sub/Item`` that is the *parent* menu, just
|
|
275
|
+
before ``Sub`` is created -- checking the freshly made (and therefore
|
|
276
|
+
empty) leaf instead placed no divider at all, and the next sibling
|
|
277
|
+
entry then saw an untagged submenu as the last action and inserted the
|
|
278
|
+
divider after the plugin submenu, splitting the plugin group instead of
|
|
279
|
+
separating it from the native entries.
|
|
280
|
+
"""
|
|
281
|
+
actions = menu.actions()
|
|
282
|
+
if (
|
|
283
|
+
actions
|
|
284
|
+
and not actions[-1].isSeparator()
|
|
285
|
+
and actions[-1].data() != self._PLUGIN_ACTION_TAG
|
|
286
|
+
):
|
|
287
|
+
sep = menu.addSeparator()
|
|
288
|
+
if sep is not None:
|
|
289
|
+
sep.setData(self._PLUGIN_ACTION_TAG)
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def _wants_header_slot(
|
|
293
|
+
cls, top_level_title: str, parts: List[str], action_def: Dict[str, Any]
|
|
294
|
+
) -> bool:
|
|
295
|
+
"""Return True for an entry that asked to sit beside the Plugin Manager.
|
|
296
|
+
|
|
297
|
+
A plugin that manages the plugin system itself — the Plugin Installer —
|
|
298
|
+
reads as part of the menu's header pair rather than as one more
|
|
299
|
+
installed plugin, and says so with ``pin="header"``. The app therefore
|
|
300
|
+
needs to know no plugin by name.
|
|
301
|
+
"""
|
|
302
|
+
return (
|
|
303
|
+
top_level_title == cls._PLUGIN_MENU_TITLE
|
|
304
|
+
and len(parts) == 2
|
|
305
|
+
and action_def.get("pin") == cls._PIN_HEADER
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def _trim_trailing_separator(menu: QMenu) -> None:
|
|
310
|
+
"""Drop a divider left dangling at the end of *menu*."""
|
|
311
|
+
actions = menu.actions()
|
|
312
|
+
if actions and actions[-1].isSeparator():
|
|
313
|
+
menu.removeAction(actions[-1])
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def _add_beside_plugin_manager(menu: QMenu, action: QAction) -> None:
|
|
317
|
+
"""Add *action* to the menu's header group, above the first divider."""
|
|
318
|
+
anchor = next((a for a in menu.actions() if a.isSeparator()), None)
|
|
319
|
+
if anchor is None:
|
|
320
|
+
menu.addAction(action)
|
|
321
|
+
else:
|
|
322
|
+
menu.insertAction(anchor, action)
|
|
323
|
+
|
|
176
324
|
def add_registered_plugin_actions(self) -> None:
|
|
177
325
|
"""Add menu actions explicitly registered by V3/V4 plugins."""
|
|
178
326
|
if not self._im.host.plugin_manager.menu_actions:
|
|
@@ -211,25 +359,31 @@ class PluginMenuManager:
|
|
|
211
359
|
),
|
|
212
360
|
None,
|
|
213
361
|
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
362
|
+
if sub is not None:
|
|
363
|
+
# Descending into an existing submenu appends nothing to
|
|
364
|
+
# the parent, so no divider is due here.
|
|
365
|
+
current_menu = sub
|
|
366
|
+
continue
|
|
367
|
+
self._ensure_plugin_separator(current_menu)
|
|
368
|
+
current_menu = current_menu.addMenu(part)
|
|
369
|
+
current_menu.menuAction().setData(self._PLUGIN_ACTION_TAG)
|
|
370
|
+
|
|
371
|
+
label = text or parts[-1]
|
|
372
|
+
pinned = self._wants_header_slot(top_level_title, parts, action_def)
|
|
373
|
+
if not pinned:
|
|
374
|
+
self._ensure_plugin_separator(current_menu)
|
|
224
375
|
|
|
225
|
-
action = QAction(
|
|
376
|
+
action = QAction(label, self._im.host)
|
|
226
377
|
action.triggered.connect(
|
|
227
378
|
self._make_safe_callback(callback, action_def.get("plugin", "Plugin"))
|
|
228
379
|
)
|
|
229
380
|
if action_def.get("shortcut"):
|
|
230
381
|
action.setShortcut(QKeySequence(action_def["shortcut"]))
|
|
231
382
|
action.setData(self._PLUGIN_ACTION_TAG)
|
|
232
|
-
|
|
383
|
+
if pinned:
|
|
384
|
+
self._add_beside_plugin_manager(current_menu, action)
|
|
385
|
+
else:
|
|
386
|
+
current_menu.addAction(action)
|
|
233
387
|
|
|
234
388
|
def add_plugin_toolbar_actions(self) -> None:
|
|
235
389
|
"""Populate the plugin toolbar from registered toolbar actions."""
|
|
@@ -271,21 +425,17 @@ class PluginMenuManager:
|
|
|
271
425
|
dlg.exec()
|
|
272
426
|
self.update_plugin_menu(plugin_menu)
|
|
273
427
|
|
|
274
|
-
def _clear_all_plugin_actions(self
|
|
275
|
-
"""Remove all tagged plugin actions from every menu and the export button.
|
|
276
|
-
|
|
277
|
-
def clear_menu(menu: Any) -> None:
|
|
278
|
-
if not menu:
|
|
279
|
-
return
|
|
280
|
-
for act in list(menu.actions()):
|
|
281
|
-
if act.data() == self._PLUGIN_ACTION_TAG:
|
|
282
|
-
self._remove_action(menu, act)
|
|
283
|
-
elif act.menu():
|
|
284
|
-
clear_menu(act.menu())
|
|
428
|
+
def _clear_all_plugin_actions(self) -> None:
|
|
429
|
+
"""Remove all tagged plugin actions from every menu and the export button.
|
|
285
430
|
|
|
286
|
-
|
|
431
|
+
The Plugin menu itself is not emptied here — ``_reset_plugin_menu``
|
|
432
|
+
owns that, so both the update and the rebuild path clear it the same
|
|
433
|
+
way.
|
|
434
|
+
"""
|
|
287
435
|
for menu in self._get_plugin_target_menus():
|
|
288
|
-
|
|
436
|
+
if menu:
|
|
437
|
+
self._strip_plugin_actions(menu)
|
|
438
|
+
self._clear_plugin_menubar_entries()
|
|
289
439
|
|
|
290
440
|
def update_style_menu_with_plugins(self) -> None:
|
|
291
441
|
"""Append custom 3D styles registered by plugins to the style menu."""
|
|
@@ -329,15 +479,36 @@ class PluginMenuManager:
|
|
|
329
479
|
root = []
|
|
330
480
|
for p in plugins:
|
|
331
481
|
if hasattr(p["module"], "run"):
|
|
332
|
-
cat = p.get("category"
|
|
482
|
+
cat = str(p.get("category") or "").strip()
|
|
333
483
|
if cat:
|
|
334
484
|
categorized.setdefault(cat, []).append(p)
|
|
335
485
|
else:
|
|
336
486
|
root.append(p)
|
|
337
487
|
|
|
488
|
+
# This is the last step of Plugin-menu construction on both the update
|
|
489
|
+
# and the rebuild path, so it also drops the header divider when there
|
|
490
|
+
# turns out to be nothing under it -- an install of only the Plugin
|
|
491
|
+
# Installer would otherwise end on a dangling divider.
|
|
492
|
+
if not categorized and not root:
|
|
493
|
+
self._trim_trailing_separator(plugin_menu)
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
# Divide the folder-derived entries below from the context-injected
|
|
497
|
+
# ones ("Plugin/XXX" paths) that add_registered_plugin_actions already
|
|
498
|
+
# placed above: two different organizing schemes should not read as one
|
|
499
|
+
# list. Deferred until we know there is content, so an app with only
|
|
500
|
+
# context-injected plugins gets no dangling trailing divider.
|
|
501
|
+
existing = plugin_menu.actions()
|
|
502
|
+
if existing and not existing[-1].isSeparator():
|
|
503
|
+
plugin_menu.addSeparator()
|
|
504
|
+
|
|
338
505
|
for cat in sorted(categorized.keys()):
|
|
339
506
|
current_parent: Any = plugin_menu
|
|
340
|
-
|
|
507
|
+
# A folder category arrives with os.sep; a root plugin's own
|
|
508
|
+
# PLUGIN_CATEGORY is hand-written and usually uses "/".
|
|
509
|
+
for part in cat.replace("/", os.sep).split(os.sep):
|
|
510
|
+
if not part:
|
|
511
|
+
continue
|
|
341
512
|
sub = next(
|
|
342
513
|
(
|
|
343
514
|
a.menu()
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/template_preview_item.py
RENAMED
|
@@ -54,6 +54,7 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
54
54
|
self.ghost_atoms: List[AtomItem] = []
|
|
55
55
|
self.ghost_bonds: List[BondItem] = []
|
|
56
56
|
self.existing_indices: Set[int] = set()
|
|
57
|
+
self.editor_drawn_bonds: Set[frozenset] = set()
|
|
57
58
|
self.existing_h_counts: Dict[int, int] = {}
|
|
58
59
|
self.replaced_label_path = QPainterPath()
|
|
59
60
|
self.mark_first_atom = False
|
|
@@ -66,11 +67,13 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
66
67
|
points: list[QPointF],
|
|
67
68
|
is_aromatic: bool = False,
|
|
68
69
|
bonds_info: Optional[list[Any]] = None,
|
|
70
|
+
editor_drawn_bonds: Optional[list[Any]] = None,
|
|
69
71
|
) -> None:
|
|
70
72
|
"""Set polygon points for a standard ring template preview.
|
|
71
73
|
|
|
72
74
|
``bonds_info`` carries the orders the placement will use; without it the
|
|
73
|
-
ring falls back to a plain alternation.
|
|
75
|
+
ring falls back to a plain alternation. ``editor_drawn_bonds`` lists the
|
|
76
|
+
vertex pairs the editor already draws, which the ghost must leave alone.
|
|
74
77
|
"""
|
|
75
78
|
self.prepareGeometryChange()
|
|
76
79
|
self.is_chain = False
|
|
@@ -83,6 +86,10 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
83
86
|
for i in range(n)
|
|
84
87
|
]
|
|
85
88
|
self._rebuild_ghost(points, bonds_info, [{"symbol": "C"} for _ in points])
|
|
89
|
+
# After the rebuild, which clears whatever the last preview recorded
|
|
90
|
+
self.editor_drawn_bonds = {
|
|
91
|
+
frozenset(pair) for pair in (editor_drawn_bonds or ())
|
|
92
|
+
}
|
|
86
93
|
self.update()
|
|
87
94
|
|
|
88
95
|
def set_chain_geometry(
|
|
@@ -130,6 +137,7 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
130
137
|
"""Drop the ghost molecule and everything derived from the editor scene."""
|
|
131
138
|
self._clear_ghost_items()
|
|
132
139
|
self.existing_indices = set()
|
|
140
|
+
self.editor_drawn_bonds = set()
|
|
133
141
|
self.existing_h_counts = {}
|
|
134
142
|
self.replaced_label_path = QPainterPath()
|
|
135
143
|
|
|
@@ -140,6 +148,9 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
140
148
|
atoms_data: list[dict[str, Any]],
|
|
141
149
|
) -> None:
|
|
142
150
|
"""Rebuild the preview as real atom/bond items so it renders like the result."""
|
|
151
|
+
# Every preview kind lands here, so this is the one place that can keep
|
|
152
|
+
# a user template from inheriting the pairs a fused ring recorded
|
|
153
|
+
self.editor_drawn_bonds = set()
|
|
143
154
|
self.ghost_scene.host = self.scene()
|
|
144
155
|
if not points:
|
|
145
156
|
self._clear_ghost()
|
|
@@ -362,6 +373,11 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
362
373
|
painter.save()
|
|
363
374
|
painter.setOpacity(GHOST_OPACITY)
|
|
364
375
|
for bond in self.ghost_bonds:
|
|
376
|
+
pair = frozenset((bond.atom1.atom_id, bond.atom2.atom_id))
|
|
377
|
+
if pair in self.editor_drawn_bonds:
|
|
378
|
+
# Placement leaves this bond alone and the editor already draws
|
|
379
|
+
# it; a ghost copy on top of a real double bond reads as a triple
|
|
380
|
+
continue
|
|
365
381
|
painter.save()
|
|
366
382
|
painter.translate(bond.pos())
|
|
367
383
|
bond.paint(painter, option) # type: ignore[arg-type]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/MoleditPy_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/MoleditPy_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/plugins/plugin_manager_window.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/align_plane_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/base_picking_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/bond_length_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/color_settings_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/custom_interactor_style.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/custom_qt_interactor.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/geometry_base_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/periodic_table_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/settings_tabs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/template_preview_view.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/translation_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/ui/user_template_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/utils/default_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.8.0 → moleditpy_linux-4.8.2}/src/moleditpy_linux/utils/sip_isdeleted_safe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|