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.
Files changed (64) hide show
  1. {moleditpy-2.5.1 → moleditpy-2.5.2}/PKG-INFO +1 -1
  2. {moleditpy-2.5.1 → moleditpy-2.5.2}/pyproject.toml +1 -1
  3. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/__main__.py +0 -3
  5. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/main.py +0 -3
  6. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/__init__.py +3 -3
  7. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/about_dialog.py +1 -1
  8. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/align_plane_dialog.py +2 -2
  9. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/alignment_dialog.py +2 -2
  10. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/angle_dialog.py +2 -3
  11. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/bond_item.py +2 -4
  12. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/bond_length_dialog.py +3 -3
  13. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/calculation_worker.py +5 -36
  14. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/color_settings_dialog.py +1 -6
  15. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/constants.py +1 -1
  16. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/constrained_optimization_dialog.py +2 -5
  17. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/custom_interactor_style.py +1 -36
  18. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/custom_qt_interactor.py +0 -6
  19. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -5
  20. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/dihedral_dialog.py +2 -3
  21. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window.py +2 -47
  22. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_app_state.py +4 -35
  23. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_compute.py +19 -57
  24. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_dialog_manager.py +3 -47
  25. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_edit_3d.py +0 -44
  26. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_edit_actions.py +0 -65
  27. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_export.py +20 -49
  28. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_main_init.py +18 -91
  29. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_molecular_parsers.py +34 -80
  30. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_project_io.py +33 -71
  31. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_string_importers.py +0 -21
  32. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_ui_manager.py +0 -54
  33. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_view_3d.py +0 -71
  34. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/main_window_view_loaders.py +0 -16
  35. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/mirror_dialog.py +2 -2
  36. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/molecule_scene.py +22 -48
  37. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/move_group_dialog.py +5 -6
  38. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/periodic_table_dialog.py +1 -1
  39. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/planarize_dialog.py +1 -1
  40. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/plugin_interface.py +0 -9
  41. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/plugin_manager_window.py +2 -10
  42. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/settings_dialog.py +1 -12
  43. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/template_preview_item.py +0 -1
  44. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/translation_dialog.py +2 -5
  45. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/user_template_dialog.py +2 -2
  46. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/zoomable_view.py +0 -1
  47. {moleditpy-2.5.1 → moleditpy-2.5.2}/LICENSE +0 -0
  48. {moleditpy-2.5.1 → moleditpy-2.5.2}/README.md +0 -0
  49. {moleditpy-2.5.1 → moleditpy-2.5.2}/setup.cfg +0 -0
  50. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  51. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  52. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  53. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/requires.txt +0 -0
  54. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/MoleditPy.egg-info/top_level.txt +0 -0
  55. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/__init__.py +0 -0
  56. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/analysis_window.py +0 -0
  57. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/assets/file_icon.ico +0 -0
  58. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/assets/icon.icns +0 -0
  59. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/assets/icon.ico +0 -0
  60. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/assets/icon.png +0 -0
  61. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/atom_item.py +0 -0
  62. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/molecular_data.py +0 -0
  63. {moleditpy-2.5.1 → moleditpy-2.5.2}/src/moleditpy/modules/plugin_manager.py +0 -0
  64. {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.1
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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "2.5.1"
8
+ version = "2.5.2"
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: 2.5.1
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) # emit (worker_id, msg) tuples for robustness
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()
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QFont, QColor
16
16
  from rdkit import Chem
17
17
 
18
18
  #Version
19
- VERSION = '2.5.1'
19
+ VERSION = '2.5.2'
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -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
- # Note: We do NOT set _mouse_press_pos here initially.
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):