moleditpy 2.5.1__tar.gz → 2.5.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-2.5.1 → moleditpy-2.5.2}/PKG-INFO +1 -1
- {moleditpy-2.5.1 → moleditpy-2.5.2}/pyproject.toml +1 -1
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/__main__.py +0 -3
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/main.py +0 -3
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/__init__.py +3 -3
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/about_dialog.py +1 -1
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/align_plane_dialog.py +2 -2
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/alignment_dialog.py +2 -2
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/angle_dialog.py +2 -3
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/bond_item.py +2 -4
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/bond_length_dialog.py +3 -3
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/calculation_worker.py +5 -36
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/color_settings_dialog.py +1 -6
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/constants.py +1 -1
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/constrained_optimization_dialog.py +2 -5
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/custom_interactor_style.py +1 -36
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/custom_qt_interactor.py +0 -6
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -5
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/dihedral_dialog.py +2 -3
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window.py +2 -47
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_app_state.py +4 -35
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_compute.py +19 -57
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_dialog_manager.py +3 -47
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_edit_3d.py +0 -44
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_edit_actions.py +0 -65
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_export.py +20 -49
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_main_init.py +18 -91
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_molecular_parsers.py +34 -80
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_project_io.py +33 -71
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_string_importers.py +0 -21
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_ui_manager.py +0 -54
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_view_3d.py +0 -71
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_view_loaders.py +0 -16
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/mirror_dialog.py +2 -2
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/molecule_scene.py +22 -48
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/move_group_dialog.py +5 -6
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/periodic_table_dialog.py +1 -1
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/planarize_dialog.py +1 -1
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/plugin_interface.py +0 -9
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/plugin_manager_window.py +2 -10
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/settings_dialog.py +1 -12
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/template_preview_item.py +0 -1
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/translation_dialog.py +2 -5
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/user_template_dialog.py +2 -2
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/zoomable_view.py +0 -1
- {moleditpy-2.5.1 → moleditpy-2.5.2}/LICENSE +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/README.md +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/setup.cfg +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/__init__.py +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/analysis_window.py +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/assets/file_icon.ico +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/assets/icon.icns +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/assets/icon.ico +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/assets/icon.png +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/atom_item.py +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/molecular_data.py +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/plugin_manager.py +0 -0
- {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/template_preview_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -16,11 +16,8 @@ print("MoleditPy — A Python-based molecular editing software")
|
|
|
16
16
|
print("-----------------------------------------------------\n")
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
|
-
# Preferred when running as a package: python -m moleditpy
|
|
20
19
|
from .main import main
|
|
21
20
|
except Exception:
|
|
22
|
-
# Fallback when running the file directly: python __main__.py
|
|
23
|
-
# This will import the top-level `main` module in the same folder.
|
|
24
21
|
from main import main
|
|
25
22
|
|
|
26
23
|
# --- Application Execution ---
|
|
@@ -17,11 +17,8 @@ import ctypes
|
|
|
17
17
|
from PyQt6.QtWidgets import QApplication
|
|
18
18
|
|
|
19
19
|
try:
|
|
20
|
-
# When executed as part of the package (python -m moleditpy)
|
|
21
20
|
from .modules.main_window import MainWindow
|
|
22
21
|
except Exception:
|
|
23
|
-
# When executed as a standalone script (python main.py) the package-relative
|
|
24
|
-
# import won't work; fall back to absolute import that works with sys.path
|
|
25
22
|
from modules.main_window import MainWindow
|
|
26
23
|
|
|
27
24
|
def main():
|
|
@@ -15,7 +15,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
15
15
|
# Do not import `pybel` at module import time. Only expose the presence
|
|
16
16
|
# of Open Babel via `OBABEL_AVAILABLE`. Modules should import `pybel`
|
|
17
17
|
# lazily if and when they need it.
|
|
18
|
-
try:
|
|
18
|
+
try: # pragma: no cover
|
|
19
19
|
import importlib.util
|
|
20
20
|
OBABEL_AVAILABLE = importlib.util.find_spec("openbabel") is not None
|
|
21
21
|
except Exception:
|
|
@@ -26,7 +26,7 @@ except Exception:
|
|
|
26
26
|
# it once at module import time and expose a small, robust wrapper so callers
|
|
27
27
|
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
28
28
|
# handling (this reduces crash risk during teardown and deletion operations).
|
|
29
|
-
try:
|
|
29
|
+
try: # pragma: no cover
|
|
30
30
|
import sip as _sip # type: ignore
|
|
31
31
|
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
32
32
|
except Exception:
|
|
@@ -40,7 +40,7 @@ def sip_isdeleted_safe(obj) -> bool:
|
|
|
40
40
|
occurs while checking, it returns False (i.e. not deleted) so that the
|
|
41
41
|
caller can continue other lightweight guards (like checking scene()).
|
|
42
42
|
"""
|
|
43
|
-
try:
|
|
43
|
+
try: # pragma: no cover
|
|
44
44
|
if _sip_isdeleted is None:
|
|
45
45
|
return False
|
|
46
46
|
return bool(_sip_isdeleted(obj))
|
|
@@ -21,7 +21,7 @@ try:
|
|
|
21
21
|
except Exception:
|
|
22
22
|
from modules.constants import VERSION
|
|
23
23
|
|
|
24
|
-
class AboutDialog(QDialog):
|
|
24
|
+
class AboutDialog(QDialog): # pragma: no cover
|
|
25
25
|
def __init__(self, main_window, parent=None):
|
|
26
26
|
super().__init__(parent)
|
|
27
27
|
self.main_window = main_window
|
|
@@ -21,7 +21,7 @@ try:
|
|
|
21
21
|
except Exception:
|
|
22
22
|
from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
23
23
|
|
|
24
|
-
class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
|
|
24
|
+
class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
25
25
|
def __init__(self, mol, main_window, plane, preselected_atoms=None, parent=None):
|
|
26
26
|
QDialog.__init__(self, parent)
|
|
27
27
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -44,7 +44,7 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog):
|
|
|
44
44
|
def init_ui(self):
|
|
45
45
|
plane_names = {'xy': 'XY', 'xz': 'XZ', 'yz': 'YZ'}
|
|
46
46
|
self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
|
|
47
|
-
self.setModal(False)
|
|
47
|
+
self.setModal(False)
|
|
48
48
|
layout = QVBoxLayout(self)
|
|
49
49
|
|
|
50
50
|
# Instructions
|
|
@@ -22,7 +22,7 @@ try:
|
|
|
22
22
|
except Exception:
|
|
23
23
|
from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
24
24
|
|
|
25
|
-
class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
25
|
+
class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
26
26
|
def __init__(self, mol, main_window, axis, preselected_atoms=None, parent=None):
|
|
27
27
|
QDialog.__init__(self, parent)
|
|
28
28
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -46,7 +46,7 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
46
46
|
def init_ui(self):
|
|
47
47
|
axis_names = {'x': 'X-axis', 'y': 'Y-axis', 'z': 'Z-axis'}
|
|
48
48
|
self.setWindowTitle(f"Align to {axis_names[self.axis]}")
|
|
49
|
-
self.setModal(False)
|
|
49
|
+
self.setModal(False)
|
|
50
50
|
layout = QVBoxLayout(self)
|
|
51
51
|
|
|
52
52
|
# Instructions
|
|
@@ -24,7 +24,7 @@ from PyQt6.QtWidgets import QMessageBox
|
|
|
24
24
|
import numpy as np
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class AngleDialog(Dialog3DPickingMixin, QDialog):
|
|
27
|
+
class AngleDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
28
28
|
def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
|
|
29
29
|
QDialog.__init__(self, parent)
|
|
30
30
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -44,8 +44,7 @@ class AngleDialog(Dialog3DPickingMixin, QDialog):
|
|
|
44
44
|
|
|
45
45
|
def init_ui(self):
|
|
46
46
|
self.setWindowTitle("Adjust Angle")
|
|
47
|
-
self.setModal(False)
|
|
48
|
-
# 常に前面表示
|
|
47
|
+
self.setModal(False)
|
|
49
48
|
layout = QVBoxLayout(self)
|
|
50
49
|
|
|
51
50
|
# Instructions
|
|
@@ -34,6 +34,8 @@ except Exception:
|
|
|
34
34
|
HOVER_PEN_WIDTH, DESIRED_BOND_PIXEL_WIDTH,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
+
from rdkit import Chem
|
|
38
|
+
|
|
37
39
|
class BondItem(QGraphicsItem):
|
|
38
40
|
|
|
39
41
|
def get_ez_label_rect(self):
|
|
@@ -206,9 +208,6 @@ class BondItem(QGraphicsItem):
|
|
|
206
208
|
except Exception:
|
|
207
209
|
return None
|
|
208
210
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
211
|
def paint(self, painter, option, widget):
|
|
213
212
|
if self.atom1 is None or self.atom2 is None:
|
|
214
213
|
return
|
|
@@ -341,7 +340,6 @@ class BondItem(QGraphicsItem):
|
|
|
341
340
|
if bond and bond.IsInRing():
|
|
342
341
|
is_in_ring = True
|
|
343
342
|
# 環の中心を計算(この結合を含む最小環)
|
|
344
|
-
from rdkit import Chem
|
|
345
343
|
ring_info = mol.GetRingInfo()
|
|
346
344
|
for ring in ring_info.AtomRings():
|
|
347
345
|
if rdkit_idx1 in ring and rdkit_idx2 in ring:
|
|
@@ -19,7 +19,7 @@ from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
|
19
19
|
from PyQt6.QtCore import Qt
|
|
20
20
|
import numpy as np
|
|
21
21
|
|
|
22
|
-
class BondLengthDialog(Dialog3DPickingMixin, QDialog):
|
|
22
|
+
class BondLengthDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
23
23
|
def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
|
|
24
24
|
QDialog.__init__(self, parent)
|
|
25
25
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -37,8 +37,8 @@ class BondLengthDialog(Dialog3DPickingMixin, QDialog):
|
|
|
37
37
|
|
|
38
38
|
def init_ui(self):
|
|
39
39
|
self.setWindowTitle("Adjust Bond Length")
|
|
40
|
-
self.setModal(False)
|
|
41
|
-
|
|
40
|
+
self.setModal(False)
|
|
41
|
+
|
|
42
42
|
layout = QVBoxLayout(self)
|
|
43
43
|
|
|
44
44
|
# Instructions
|
|
@@ -22,12 +22,6 @@ from rdkit.DistanceGeometry import DoTriangleSmoothing
|
|
|
22
22
|
import math
|
|
23
23
|
import re
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
# Use centralized Open Babel availability from package-level __init__
|
|
27
|
-
# Use per-package modules availability (local __init__).
|
|
28
|
-
# Prefer package-relative import when running as `python -m moleditpy` and
|
|
29
|
-
# fall back to a top-level import when running as a script. This mirrors the
|
|
30
|
-
# import style used in other modules and keeps the package robust.
|
|
31
25
|
try:
|
|
32
26
|
from . import OBABEL_AVAILABLE
|
|
33
27
|
except Exception:
|
|
@@ -47,22 +41,14 @@ else:
|
|
|
47
41
|
class CalculationWorker(QObject):
|
|
48
42
|
status_update = pyqtSignal(str)
|
|
49
43
|
finished = pyqtSignal(object)
|
|
50
|
-
error = pyqtSignal(object)
|
|
51
|
-
# Per-worker start signal to avoid sharing a single MainWindow signal
|
|
52
|
-
# among many worker instances (which causes race conditions and stale
|
|
53
|
-
# workers being started on a single emission).
|
|
44
|
+
error = pyqtSignal(object)
|
|
54
45
|
start_work = pyqtSignal(str, object)
|
|
55
46
|
|
|
56
47
|
def __init__(self, parent=None):
|
|
57
48
|
super().__init__(parent)
|
|
58
|
-
# Connect the worker's own start signal to its run slot. This
|
|
59
|
-
# guarantees that only this worker will respond when start_work
|
|
60
|
-
# is emitted (prevents cross-talk between workers).
|
|
61
49
|
try:
|
|
62
50
|
self.start_work.connect(self.run_calculation)
|
|
63
51
|
except Exception:
|
|
64
|
-
# Be defensive: if connection fails, continue; the caller may
|
|
65
|
-
# fallback to emitting directly.
|
|
66
52
|
pass
|
|
67
53
|
|
|
68
54
|
@pyqtSlot(str, object)
|
|
@@ -75,11 +61,7 @@ class CalculationWorker(QObject):
|
|
|
75
61
|
worker_id = options.get('worker_id') if options else None
|
|
76
62
|
except Exception:
|
|
77
63
|
worker_id = None
|
|
78
|
-
|
|
79
|
-
# If a caller starts a worker without providing a worker_id, treat
|
|
80
|
-
# it as a "global" worker that can still be halted via a global
|
|
81
|
-
# halt flag. Emit a single status warning so callers know that
|
|
82
|
-
# the worker was started without an identifier.
|
|
64
|
+
|
|
83
65
|
_warned_no_worker_id = False
|
|
84
66
|
if worker_id is None:
|
|
85
67
|
try:
|
|
@@ -92,9 +74,6 @@ class CalculationWorker(QObject):
|
|
|
92
74
|
def _check_halted():
|
|
93
75
|
try:
|
|
94
76
|
halt_ids = getattr(self, 'halt_ids', None)
|
|
95
|
-
# If worker_id is None, allow halting via a global mechanism:
|
|
96
|
-
# - an explicit attribute `halt_all` set to True on the worker
|
|
97
|
-
# - the shared `halt_ids` set containing None or the sentinel 'ALL'
|
|
98
77
|
if worker_id is None:
|
|
99
78
|
if getattr(self, 'halt_all', False):
|
|
100
79
|
return True
|
|
@@ -115,11 +94,11 @@ class CalculationWorker(QObject):
|
|
|
115
94
|
if _check_halted():
|
|
116
95
|
return
|
|
117
96
|
self.status_update.emit(msg)
|
|
118
|
-
except Exception:
|
|
97
|
+
except Exception: # pragma: no cover
|
|
119
98
|
# Swallow any signal-emission errors to avoid crashing the worker
|
|
120
99
|
pass
|
|
121
100
|
|
|
122
|
-
def _safe_finished(payload):
|
|
101
|
+
def _safe_finished(payload): # pragma: no cover
|
|
123
102
|
try:
|
|
124
103
|
# Attempt to emit the payload; preserve existing fallback behavior
|
|
125
104
|
try:
|
|
@@ -137,7 +116,7 @@ class CalculationWorker(QObject):
|
|
|
137
116
|
except Exception:
|
|
138
117
|
pass
|
|
139
118
|
|
|
140
|
-
def _safe_error(msg):
|
|
119
|
+
def _safe_error(msg): # pragma: no cover
|
|
141
120
|
try:
|
|
142
121
|
# Emit a tuple containing the worker_id (may be None) and the message
|
|
143
122
|
try:
|
|
@@ -155,10 +134,6 @@ class CalculationWorker(QObject):
|
|
|
155
134
|
if options is None:
|
|
156
135
|
options = {}
|
|
157
136
|
conversion_mode = options.get('conversion_mode', 'fallback')
|
|
158
|
-
# Ensure params exists in all code paths (some RDKit calls below
|
|
159
|
-
# reference `params` and earlier editing introduced a path where
|
|
160
|
-
# it might not be defined). Initialize to None here and assign
|
|
161
|
-
# a proper ETKDG params object later where needed.
|
|
162
137
|
params = None
|
|
163
138
|
if not mol_block:
|
|
164
139
|
raise ValueError("No atoms to convert.")
|
|
@@ -173,8 +148,6 @@ class CalculationWorker(QObject):
|
|
|
173
148
|
if _check_halted():
|
|
174
149
|
raise RuntimeError("Halted")
|
|
175
150
|
|
|
176
|
-
# CRITICAL FIX: Extract and restore explicit E/Z labels from MOL block
|
|
177
|
-
# Parse M CFG lines to get explicit stereo labels
|
|
178
151
|
explicit_stereo = {}
|
|
179
152
|
mol_lines = mol_block.split('\n')
|
|
180
153
|
for line in mol_lines:
|
|
@@ -552,10 +525,6 @@ class CalculationWorker(QObject):
|
|
|
552
525
|
stereo_atoms = bond.GetStereoAtoms()
|
|
553
526
|
original_stereo_info.append((bond.GetIdx(), bond.GetStereo(), stereo_atoms))
|
|
554
527
|
|
|
555
|
-
# Only report RDKit-specific messages when RDKit embedding will be
|
|
556
|
-
# attempted. For other conversion modes, emit clearer, non-misleading
|
|
557
|
-
# status messages so the UI doesn't show "RDKit" when e.g. direct
|
|
558
|
-
# coordinates or Open Babel will be used.
|
|
559
528
|
if conversion_mode in ('fallback', 'rdkit'):
|
|
560
529
|
_safe_status("RDKit: Embedding 3D coordinates...")
|
|
561
530
|
elif conversion_mode == 'obabel':
|
|
@@ -20,7 +20,7 @@ try:
|
|
|
20
20
|
except Exception:
|
|
21
21
|
from modules.constants import CPK_COLORS, DEFAULT_CPK_COLORS
|
|
22
22
|
|
|
23
|
-
class ColorSettingsDialog(QDialog):
|
|
23
|
+
class ColorSettingsDialog(QDialog): # pragma: no cover
|
|
24
24
|
"""Dialog to customize CPK element colors.
|
|
25
25
|
|
|
26
26
|
- Click an element to pick a new color for the element (CPK colors).
|
|
@@ -126,11 +126,6 @@ class ColorSettingsDialog(QDialog):
|
|
|
126
126
|
h.addWidget(apply_button); h.addWidget(ok_button); h.addWidget(cancel_button)
|
|
127
127
|
layout.addLayout(h)
|
|
128
128
|
|
|
129
|
-
# initialize
|
|
130
|
-
# No 2D bond color control here
|
|
131
|
-
|
|
132
|
-
# 2D bond color picker removed — 2D bond color is fixed and not configurable here
|
|
133
|
-
|
|
134
129
|
def on_element_clicked(self):
|
|
135
130
|
b = self.sender()
|
|
136
131
|
symbol = b.text()
|
{moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/constrained_optimization_dialog.py
RENAMED
|
@@ -21,7 +21,7 @@ from PyQt6.QtCore import Qt
|
|
|
21
21
|
from rdkit.Chem import AllChem, rdMolTransforms
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
24
|
+
class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
25
25
|
"""制約付き最適化ダイアログ"""
|
|
26
26
|
|
|
27
27
|
def __init__(self, mol, main_window, parent=None):
|
|
@@ -90,8 +90,6 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
90
90
|
current_method_str = self.main_window.optimization_method or "MMFF_RDKIT"
|
|
91
91
|
current_method = current_method_str.upper()
|
|
92
92
|
|
|
93
|
-
# (修正) 比較順序を厳密化
|
|
94
|
-
|
|
95
93
|
# 1. UFF_RDKIT
|
|
96
94
|
if current_method == "UFF_RDKIT":
|
|
97
95
|
self.ff_combo.setCurrentText("UFF")
|
|
@@ -278,8 +276,7 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
278
276
|
return
|
|
279
277
|
|
|
280
278
|
self.constraints.append((constraint_type, atom_indices, value, force_const))
|
|
281
|
-
|
|
282
|
-
# 表を更新
|
|
279
|
+
|
|
283
280
|
# 表を更新
|
|
284
281
|
row_count = self.constraint_table.rowCount()
|
|
285
282
|
self.constraint_table.insertRow(row_count)
|
|
@@ -171,9 +171,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
171
171
|
# 測定モードが有効な場合の処理
|
|
172
172
|
if mw.measurement_mode and mw.current_mol:
|
|
173
173
|
click_pos = self.GetInteractor().GetEventPosition()
|
|
174
|
-
|
|
175
|
-
# We only set it if we confirm it's a background click (see below).
|
|
176
|
-
self._mouse_moved_during_drag = False # Reset drag flag
|
|
174
|
+
self._mouse_moved_during_drag = False
|
|
177
175
|
|
|
178
176
|
picker = mw.plotter.picker
|
|
179
177
|
|
|
@@ -212,10 +210,6 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
212
210
|
super().OnLeftButtonDown()
|
|
213
211
|
return
|
|
214
212
|
|
|
215
|
-
# Ctrl+クリックの原子選択機能は無効化(Move Group機能で代替)
|
|
216
|
-
# if is_ctrl_click and mw.current_mol:
|
|
217
|
-
# ... (無効化)
|
|
218
|
-
|
|
219
213
|
# 3D分子(mw.current_mol)が存在する場合のみ、原子の選択処理を実行
|
|
220
214
|
if is_edit_active and mw.current_mol:
|
|
221
215
|
click_pos = self.GetInteractor().GetEventPosition()
|
|
@@ -372,14 +366,6 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
372
366
|
# カスタムの原子ドラッグ処理
|
|
373
367
|
self.is_dragging = True
|
|
374
368
|
atom_id = mw.dragged_atom_info['id']
|
|
375
|
-
# We intentionally do NOT update visible coordinates or the
|
|
376
|
-
# authoritative atom position during mouse-move while dragging.
|
|
377
|
-
# The UX requirement here is that atoms need not visibly move
|
|
378
|
-
# while the mouse is being dragged. Compute and apply the final
|
|
379
|
-
# world-coordinate only once on mouse release (on_left_button_up).
|
|
380
|
-
# Keep minimal state: mark that a drag occurred (is_dragging)
|
|
381
|
-
# and allow the release handler to compute the final position.
|
|
382
|
-
# This avoids duplicate updates and simplifies event ordering.
|
|
383
369
|
else:
|
|
384
370
|
# カメラ回転処理を親クラスに任せます
|
|
385
371
|
super().OnMouseMove()
|
|
@@ -502,13 +488,6 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
502
488
|
if self.is_dragging:
|
|
503
489
|
if mw.current_mol and mw.current_mol.GetNumConformers() > 0:
|
|
504
490
|
try:
|
|
505
|
-
# Before applying conformer updates, compute the final
|
|
506
|
-
# world coordinates for the dragged atom based on the
|
|
507
|
-
# release pointer position. During the drag we did not
|
|
508
|
-
# update mw.atom_positions_3d (to keep the visuals
|
|
509
|
-
# static). Now compute the final position for the
|
|
510
|
-
# dragged atom and store it into mw.atom_positions_3d
|
|
511
|
-
# so the conformer update loop below will pick it up.
|
|
512
491
|
atom_id = None
|
|
513
492
|
try:
|
|
514
493
|
atom_id = mw.dragged_atom_info.get('id') if mw.dragged_atom_info else None
|
|
@@ -521,10 +500,6 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
521
500
|
renderer = mw.plotter.renderer
|
|
522
501
|
current_display_pos = interactor.GetEventPosition()
|
|
523
502
|
conf = mw.current_mol.GetConformer()
|
|
524
|
-
# Use the atom's current 3D position to obtain a
|
|
525
|
-
# display-space depth (z) value, then replace the
|
|
526
|
-
# x/y with the pointer position to project back to
|
|
527
|
-
# world coordinates at that depth.
|
|
528
503
|
pos_3d = conf.GetAtomPosition(atom_id)
|
|
529
504
|
renderer.SetWorldPoint(pos_3d.x, pos_3d.y, pos_3d.z, 1.0)
|
|
530
505
|
renderer.WorldToDisplay()
|
|
@@ -538,8 +513,6 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
538
513
|
try:
|
|
539
514
|
mw.atom_positions_3d[atom_id] = new_world_coords
|
|
540
515
|
except Exception:
|
|
541
|
-
# If atom_positions_3d is immutable or shaped
|
|
542
|
-
# differently, attempt a safe conversion.
|
|
543
516
|
try:
|
|
544
517
|
ap = list(mw.atom_positions_3d)
|
|
545
518
|
ap[atom_id] = new_world_coords
|
|
@@ -547,13 +520,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
547
520
|
except Exception:
|
|
548
521
|
pass
|
|
549
522
|
except Exception:
|
|
550
|
-
# If final-position computation fails, continue
|
|
551
|
-
# and apply whatever state is available.
|
|
552
523
|
pass
|
|
553
524
|
|
|
554
|
-
# Apply the (now updated) positions to the RDKit conformer
|
|
555
|
-
# exactly once. This ensures the conformer is
|
|
556
|
-
# authoritative and avoids double-moves.
|
|
557
525
|
conf = mw.current_mol.GetConformer()
|
|
558
526
|
for i in range(mw.current_mol.GetNumAtoms()):
|
|
559
527
|
try:
|
|
@@ -575,9 +543,6 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
575
543
|
pass
|
|
576
544
|
mw.push_undo_state()
|
|
577
545
|
mw.dragged_atom_info = None
|
|
578
|
-
# Refresh overlays and labels that depend on atom_positions_3d. Do
|
|
579
|
-
# not overwrite mw.atom_positions_3d here — it already reflects the
|
|
580
|
-
# positions the user dragged to. Only update dependent displays.
|
|
581
546
|
try:
|
|
582
547
|
mw.update_3d_selection_display()
|
|
583
548
|
except Exception:
|
|
@@ -18,11 +18,6 @@ class CustomQtInteractor(QtInteractor):
|
|
|
18
18
|
def __init__(self, parent=None, main_window=None, **kwargs):
|
|
19
19
|
super().__init__(parent, **kwargs)
|
|
20
20
|
self.main_window = main_window
|
|
21
|
-
# Track recent clicks so we can detect and swallow triple-clicks
|
|
22
|
-
# Triple-clicks are not a distinct Qt event on all platforms, so we
|
|
23
|
-
# implement a small timing-based counter here and accept the event
|
|
24
|
-
# when 3 rapid clicks are detected to prevent them from reaching
|
|
25
|
-
# the VTK interactor and causing unexpected behaviour in the 3D view.
|
|
26
21
|
self._last_click_time = 0.0
|
|
27
22
|
self._click_count = 0
|
|
28
23
|
self._ignore_next_release = False
|
|
@@ -91,7 +86,6 @@ class CustomQtInteractor(QtInteractor):
|
|
|
91
86
|
else:
|
|
92
87
|
self._click_count = 2 # Force sync
|
|
93
88
|
|
|
94
|
-
# We must ignore the release event that follows this double-click event
|
|
95
89
|
self._ignore_next_release = True
|
|
96
90
|
|
|
97
91
|
try:
|
|
@@ -62,11 +62,6 @@ class Dialog3DPickingMixin:
|
|
|
62
62
|
click_threshold = vdw_radius * 1.5
|
|
63
63
|
|
|
64
64
|
if distances[closest_atom_idx] < click_threshold:
|
|
65
|
-
# We handled the pick (atom clicked) -> consume the event so
|
|
66
|
-
# other UI elements (including the VTK interactor observers)
|
|
67
|
-
# don't also process it. Set a flag on the main window so
|
|
68
|
-
# the VTK-based handlers can ignore the same logical click
|
|
69
|
-
# when it arrives via the VTK event pipeline.
|
|
70
65
|
try:
|
|
71
66
|
self.main_window._picking_consumed = True
|
|
72
67
|
except Exception:
|
|
@@ -24,7 +24,7 @@ import numpy as np
|
|
|
24
24
|
from PyQt6.QtWidgets import QMessageBox
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class DihedralDialog(Dialog3DPickingMixin, QDialog):
|
|
27
|
+
class DihedralDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
|
|
28
28
|
def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
|
|
29
29
|
QDialog.__init__(self, parent)
|
|
30
30
|
Dialog3DPickingMixin.__init__(self)
|
|
@@ -46,8 +46,7 @@ class DihedralDialog(Dialog3DPickingMixin, QDialog):
|
|
|
46
46
|
|
|
47
47
|
def init_ui(self):
|
|
48
48
|
self.setWindowTitle("Adjust Dihedral Angle")
|
|
49
|
-
self.setModal(False)
|
|
50
|
-
# 常に前面表示
|
|
49
|
+
self.setModal(False)
|
|
51
50
|
layout = QVBoxLayout(self)
|
|
52
51
|
|
|
53
52
|
# Instructions
|
|
@@ -24,15 +24,10 @@ from PyQt6.QtWidgets import (
|
|
|
24
24
|
QMainWindow
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
27
|
from PyQt6.QtCore import (
|
|
30
28
|
pyqtSignal, pyqtSlot
|
|
31
29
|
)
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
# Use centralized Open Babel availability from package-level __init__
|
|
35
|
-
# Use per-package modules availability (local __init__).
|
|
36
31
|
try:
|
|
37
32
|
from . import OBABEL_AVAILABLE
|
|
38
33
|
except Exception:
|
|
@@ -49,11 +44,6 @@ if OBABEL_AVAILABLE:
|
|
|
49
44
|
else:
|
|
50
45
|
pybel = None
|
|
51
46
|
|
|
52
|
-
# Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
|
|
53
|
-
# allows safely detecting C++ wrapper objects that have been deleted. Import
|
|
54
|
-
# it once at module import time and expose a small, robust wrapper so callers
|
|
55
|
-
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
56
|
-
# handling (this reduces crash risk during teardown and deletion operations).
|
|
57
47
|
try:
|
|
58
48
|
import sip as _sip # type: ignore
|
|
59
49
|
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
@@ -96,24 +86,12 @@ class MainWindow(QMainWindow):
|
|
|
96
86
|
|
|
97
87
|
# start_calculation carries the MOL block and an options object (second arg)
|
|
98
88
|
start_calculation = pyqtSignal(str, object)
|
|
99
|
-
def __init__(self, initial_file=None):
|
|
100
|
-
# Initialize QMainWindow to ensure underlying Qt object is prepared
|
|
101
|
-
# before helper classes interact with the window (fixes crash when
|
|
102
|
-
# QMainWindow.__init__ not called).
|
|
89
|
+
def __init__(self, initial_file=None): # pragma: no cover
|
|
103
90
|
super().__init__()
|
|
104
91
|
# --- MOVED TO main_window_main_init.py ---
|
|
105
|
-
# The window functionality has been split across several helper
|
|
106
|
-
# modules (main_window_*). Each helper is implemented as a class
|
|
107
|
-
# whose methods were originally written to operate on the
|
|
108
|
-
# MainWindow instance as 'self'. To maintain that behaviour we
|
|
109
|
-
# create a small proxy (BoundFeature) that will forward call to
|
|
110
|
-
# the helper class with the MainWindow instance as the first
|
|
111
|
-
# argument.
|
|
112
|
-
# Undo/Redo操作中に状態復元中であることを示すフラグ
|
|
113
|
-
# 他のモジュールが呼び出される前に初期化する
|
|
114
92
|
self._is_restoring_state = False
|
|
115
93
|
|
|
116
|
-
class BoundFeature:
|
|
94
|
+
class BoundFeature: # pragma: no cover
|
|
117
95
|
"""Bind a feature-class method calls to the MainWindow.
|
|
118
96
|
|
|
119
97
|
Usage:
|
|
@@ -148,9 +126,6 @@ class MainWindow(QMainWindow):
|
|
|
148
126
|
return init_method(self._host, *a, **k)
|
|
149
127
|
return None
|
|
150
128
|
|
|
151
|
-
# Attach bound helpers to the main window; this keeps the
|
|
152
|
-
# original call sites (e.g. self.main_window_compute.trigger_conversion)
|
|
153
|
-
# working without changing them.
|
|
154
129
|
self.main_window_main_init = BoundFeature(MainWindowMainInit, self)
|
|
155
130
|
self.main_window_ui_manager = BoundFeature(MainWindowUiManager, self)
|
|
156
131
|
self.main_window_view_3d = BoundFeature(MainWindowView3d, self)
|
|
@@ -165,27 +140,11 @@ class MainWindow(QMainWindow):
|
|
|
165
140
|
self.main_window_dialog_manager = BoundFeature(MainWindowDialogManager, self)
|
|
166
141
|
self.main_window_edit_3d = BoundFeature(MainWindowEdit3d, self)
|
|
167
142
|
|
|
168
|
-
# Call the initialization method from main_window_main_init which
|
|
169
|
-
# sets up the UI and the initial app state. Other helpers may
|
|
170
|
-
# expect the UI to exist so initialize them after this call.
|
|
171
143
|
try:
|
|
172
|
-
# The helper's __init__ usually accepts the initial_file
|
|
173
|
-
# argument and is expected to be invoked with the MainWindow
|
|
174
|
-
# instance as the host. Because BoundFeature itself defines
|
|
175
|
-
# __init__, attribute lookup would resolve to BoundFeature.__init__
|
|
176
|
-
# instead of the helper class method. Call the helper class
|
|
177
|
-
# __init__ explicitly with the main window instance as the
|
|
178
|
-
# first argument.
|
|
179
|
-
# Call the helper's __init__ via helper proxy to ensure the
|
|
180
|
-
# initial host and arguments are forwarded correctly.
|
|
181
144
|
self.main_window_main_init.init(initial_file)
|
|
182
145
|
except Exception:
|
|
183
|
-
# If main init fails, still continue so we can attempt
|
|
184
|
-
# to catch / repair other issues via error messages.
|
|
185
146
|
traceback.print_exc()
|
|
186
147
|
|
|
187
|
-
# Initialize other helper modules which implement their own
|
|
188
|
-
# __init__(self, main_window) if present.
|
|
189
148
|
other_inits = [
|
|
190
149
|
'main_window_view_3d', 'main_window_ui_manager', 'main_window_compute',
|
|
191
150
|
'main_window_edit_actions', 'main_window_string_importers',
|
|
@@ -195,12 +154,8 @@ class MainWindow(QMainWindow):
|
|
|
195
154
|
]
|
|
196
155
|
for name in other_inits:
|
|
197
156
|
try:
|
|
198
|
-
# Call the helper's __init__ through the proxy helper to
|
|
199
|
-
# avoid attribute-resolution issues.
|
|
200
157
|
getattr(self, name).init()
|
|
201
158
|
except Exception:
|
|
202
|
-
# Ignore; many helpers only define __init__ when they
|
|
203
|
-
# actually need it.
|
|
204
159
|
pass
|
|
205
160
|
|
|
206
161
|
def init_ui(self):
|