MoleditPy 1.15.1__py3-none-any.whl → 1.16.0__py3-none-any.whl

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 (55) hide show
  1. moleditpy/__init__.py +4 -0
  2. moleditpy/__main__.py +29 -19748
  3. moleditpy/main.py +37 -0
  4. moleditpy/modules/__init__.py +36 -0
  5. moleditpy/modules/about_dialog.py +92 -0
  6. moleditpy/modules/align_plane_dialog.py +281 -0
  7. moleditpy/modules/alignment_dialog.py +261 -0
  8. moleditpy/modules/analysis_window.py +197 -0
  9. moleditpy/modules/angle_dialog.py +428 -0
  10. moleditpy/modules/assets/icon.icns +0 -0
  11. moleditpy/modules/atom_item.py +336 -0
  12. moleditpy/modules/bond_item.py +303 -0
  13. moleditpy/modules/bond_length_dialog.py +368 -0
  14. moleditpy/modules/calculation_worker.py +754 -0
  15. moleditpy/modules/color_settings_dialog.py +309 -0
  16. moleditpy/modules/constants.py +76 -0
  17. moleditpy/modules/constrained_optimization_dialog.py +667 -0
  18. moleditpy/modules/custom_interactor_style.py +737 -0
  19. moleditpy/modules/custom_qt_interactor.py +49 -0
  20. moleditpy/modules/dialog3_d_picking_mixin.py +96 -0
  21. moleditpy/modules/dihedral_dialog.py +431 -0
  22. moleditpy/modules/main_window.py +830 -0
  23. moleditpy/modules/main_window_app_state.py +747 -0
  24. moleditpy/modules/main_window_compute.py +1203 -0
  25. moleditpy/modules/main_window_dialog_manager.py +454 -0
  26. moleditpy/modules/main_window_edit_3d.py +531 -0
  27. moleditpy/modules/main_window_edit_actions.py +1449 -0
  28. moleditpy/modules/main_window_export.py +744 -0
  29. moleditpy/modules/main_window_main_init.py +1641 -0
  30. moleditpy/modules/main_window_molecular_parsers.py +956 -0
  31. moleditpy/modules/main_window_project_io.py +429 -0
  32. moleditpy/modules/main_window_string_importers.py +270 -0
  33. moleditpy/modules/main_window_ui_manager.py +567 -0
  34. moleditpy/modules/main_window_view_3d.py +1163 -0
  35. moleditpy/modules/main_window_view_loaders.py +350 -0
  36. moleditpy/modules/mirror_dialog.py +110 -0
  37. moleditpy/modules/molecular_data.py +290 -0
  38. moleditpy/modules/molecule_scene.py +1895 -0
  39. moleditpy/modules/move_group_dialog.py +586 -0
  40. moleditpy/modules/periodic_table_dialog.py +72 -0
  41. moleditpy/modules/planarize_dialog.py +209 -0
  42. moleditpy/modules/settings_dialog.py +1034 -0
  43. moleditpy/modules/template_preview_item.py +148 -0
  44. moleditpy/modules/template_preview_view.py +62 -0
  45. moleditpy/modules/translation_dialog.py +353 -0
  46. moleditpy/modules/user_template_dialog.py +621 -0
  47. moleditpy/modules/zoomable_view.py +98 -0
  48. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/METADATA +1 -1
  49. moleditpy-1.16.0.dist-info/RECORD +54 -0
  50. moleditpy-1.15.1.dist-info/RECORD +0 -9
  51. /moleditpy/{assets → modules/assets}/icon.ico +0 -0
  52. /moleditpy/{assets → modules/assets}/icon.png +0 -0
  53. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/WHEEL +0 -0
  54. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/entry_points.txt +0 -0
  55. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ main_window_view_loaders.py
6
+ MainWindow (main_window.py) から分離されたモジュール
7
+ 機能クラス: MainWindowViewLoaders
8
+ """
9
+
10
+
11
+ import os
12
+ import traceback
13
+
14
+
15
+ # RDKit imports (explicit to satisfy flake8 and used features)
16
+ from rdkit import Chem
17
+ from rdkit.Chem import AllChem
18
+ try:
19
+ pass
20
+ except Exception:
21
+ pass
22
+
23
+ # PyQt6 Modules
24
+ from PyQt6.QtWidgets import (
25
+ QFileDialog
26
+ )
27
+
28
+
29
+
30
+
31
+
32
+ # Use centralized Open Babel availability from package-level __init__
33
+ # Use per-package modules availability (local __init__).
34
+ try:
35
+ from . import OBABEL_AVAILABLE
36
+ except Exception:
37
+ from modules import OBABEL_AVAILABLE
38
+ # Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
39
+ if OBABEL_AVAILABLE:
40
+ try:
41
+ from openbabel import pybel
42
+ except Exception:
43
+ # If import fails here, disable OBABEL locally; avoid raising
44
+ pybel = None
45
+ OBABEL_AVAILABLE = False
46
+ print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
47
+ else:
48
+ pybel = None
49
+
50
+ # Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
51
+ # allows safely detecting C++ wrapper objects that have been deleted. Import
52
+ # it once at module import time and expose a small, robust wrapper so callers
53
+ # can avoid re-importing sip repeatedly and so we centralize exception
54
+ # handling (this reduces crash risk during teardown and deletion operations).
55
+ try:
56
+ import sip as _sip # type: ignore
57
+ _sip_isdeleted = getattr(_sip, 'isdeleted', None)
58
+ except Exception:
59
+ _sip = None
60
+ _sip_isdeleted = None
61
+
62
+ try:
63
+ # package relative imports (preferred when running as `python -m moleditpy`)
64
+ from .constants import VERSION
65
+ except Exception:
66
+ # Fallback to absolute imports for script-style execution
67
+ from modules.constants import VERSION
68
+
69
+ # --- クラス定義 ---
70
+ class MainWindowViewLoaders(object):
71
+ """ main_window.py から分離された機能クラス """
72
+
73
+ def __init__(self, main_window):
74
+ """ クラスの初期化 """
75
+ self.mw = main_window
76
+
77
+ def load_xyz_for_3d_viewing(self, file_path=None):
78
+ """XYZファイルを読み込んで3Dビューアで表示する"""
79
+ if not file_path:
80
+ file_path, _ = QFileDialog.getOpenFileName(self, "Load 3D XYZ (View Only)", "", "XYZ Files (*.xyz);;All Files (*)")
81
+ if not file_path:
82
+ return
83
+
84
+ try:
85
+ mol = self.load_xyz_file(file_path)
86
+ if mol is None:
87
+ raise ValueError("Failed to create molecule from XYZ file.")
88
+ if mol.GetNumConformers() == 0:
89
+ raise ValueError("XYZ file has no 3D coordinates.")
90
+
91
+ # 2Dエディタをクリア
92
+ self.clear_2d_editor(push_to_undo=False)
93
+
94
+ # 3D構造をセットして描画
95
+ # Set the molecule. If bonds were determined (mol has bonds),
96
+ # treat this the same as loading a MOL file: clear the XYZ-derived
97
+ # flag and enable 3D optimization. Only mark as XYZ-derived and
98
+ # disable 3D optimization when the molecule has no bond information.
99
+ self.current_mol = mol
100
+
101
+ # XYZファイル読み込み時はマッピングをクリア(2D構造がないため)
102
+ self.atom_id_to_rdkit_idx_map = {}
103
+
104
+ # If the loader marked the molecule as produced under skip_chemistry_checks,
105
+ # always treat it as XYZ-derived and disable optimization. Otherwise
106
+ # fall back to the existing behavior based on bond presence.
107
+ skip_flag = False
108
+ try:
109
+ # Prefer RDKit int prop
110
+ skip_flag = bool(self.current_mol.GetIntProp("_xyz_skip_checks"))
111
+ except Exception:
112
+ try:
113
+ skip_flag = bool(getattr(self.current_mol, '_xyz_skip_checks', False))
114
+ except Exception:
115
+ skip_flag = False
116
+
117
+ if skip_flag:
118
+ self.is_xyz_derived = True
119
+ if hasattr(self, 'optimize_3d_button'):
120
+ try:
121
+ self.optimize_3d_button.setEnabled(False)
122
+ except Exception:
123
+ pass
124
+ else:
125
+ try:
126
+ has_bonds = (self.current_mol.GetNumBonds() > 0)
127
+ except Exception:
128
+ has_bonds = False
129
+
130
+ if has_bonds:
131
+ self.is_xyz_derived = False
132
+ if hasattr(self, 'optimize_3d_button'):
133
+ try:
134
+ # Only enable optimize if the molecule is not considered XYZ-derived
135
+ if not getattr(self, 'is_xyz_derived', False):
136
+ self.optimize_3d_button.setEnabled(True)
137
+ else:
138
+ self.optimize_3d_button.setEnabled(False)
139
+ except Exception:
140
+ pass
141
+ else:
142
+ self.is_xyz_derived = True
143
+ if hasattr(self, 'optimize_3d_button'):
144
+ try:
145
+ self.optimize_3d_button.setEnabled(False)
146
+ except Exception:
147
+ pass
148
+
149
+ self.draw_molecule_3d(self.current_mol)
150
+ self.plotter.reset_camera()
151
+
152
+ # UIを3Dビューアモードに設定
153
+ self._enter_3d_viewer_ui_mode()
154
+
155
+ # 3D関連機能を統一的に有効化
156
+ self._enable_3d_features(True)
157
+
158
+ # メニューテキストと状態を更新
159
+ self.update_atom_id_menu_text()
160
+ self.update_atom_id_menu_state()
161
+
162
+ self.statusBar().showMessage(f"3D Viewer Mode: Loaded {os.path.basename(file_path)}")
163
+ self.reset_undo_stack()
164
+ # XYZファイル名をcurrent_file_pathにセットし、未保存状態はFalse
165
+ self.current_file_path = file_path
166
+ self.has_unsaved_changes = False
167
+ self.update_window_title()
168
+
169
+ except FileNotFoundError:
170
+ self.statusBar().showMessage(f"File not found: {file_path}")
171
+ self.restore_ui_for_editing()
172
+ except ValueError as e:
173
+ self.statusBar().showMessage(f"Invalid XYZ file: {e}")
174
+ self.restore_ui_for_editing()
175
+ except Exception as e:
176
+ self.statusBar().showMessage(f"Error loading XYZ file: {e}")
177
+ self.restore_ui_for_editing()
178
+
179
+ traceback.print_exc()
180
+
181
+
182
+
183
+ def save_3d_as_mol(self):
184
+ if not self.current_mol:
185
+ self.statusBar().showMessage("Error: Please generate a 3D structure first.")
186
+ return
187
+
188
+ try:
189
+ # default filename based on current file
190
+ default_name = "untitled"
191
+ try:
192
+ if self.current_file_path:
193
+ base = os.path.basename(self.current_file_path)
194
+ name = os.path.splitext(base)[0]
195
+ default_name = f"{name}"
196
+ except Exception:
197
+ default_name = "untitled"
198
+
199
+ # prefer same directory as current file when available
200
+ default_path = default_name
201
+ try:
202
+ if self.current_file_path:
203
+ default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
204
+ except Exception:
205
+ default_path = default_name
206
+
207
+ file_path, _ = QFileDialog.getSaveFileName(self, "Save 3D MOL File", default_path, "MOL Files (*.mol);;All Files (*)")
208
+ if not file_path:
209
+ return
210
+
211
+ if not file_path.lower().endswith('.mol'):
212
+ file_path += '.mol'
213
+
214
+ mol_to_save = Chem.Mol(self.current_mol)
215
+
216
+ if mol_to_save.HasProp("_2D"):
217
+ mol_to_save.ClearProp("_2D")
218
+
219
+ mol_block = Chem.MolToMolBlock(mol_to_save, includeStereo=True)
220
+ lines = mol_block.split('\n')
221
+ if len(lines) > 1 and 'RDKit' in lines[1]:
222
+ lines[1] = ' MoleditPy Ver. ' + VERSION + ' 3D'
223
+ modified_mol_block = '\n'.join(lines)
224
+
225
+ with open(file_path, 'w', encoding='utf-8') as f:
226
+ f.write(modified_mol_block)
227
+ self.statusBar().showMessage(f"3D data saved to {file_path}")
228
+
229
+ except (OSError, IOError) as e:
230
+ self.statusBar().showMessage(f"File I/O error: {e}")
231
+ except UnicodeEncodeError as e:
232
+ self.statusBar().showMessage(f"Text encoding error: {e}")
233
+ except Exception as e:
234
+ self.statusBar().showMessage(f"Error saving 3D MOL file: {e}")
235
+
236
+ traceback.print_exc()
237
+
238
+
239
+
240
+ def load_mol_file_for_3d_viewing(self, file_path=None):
241
+ """MOL/SDFファイルを3Dビューアーで開く"""
242
+ if not self.check_unsaved_changes():
243
+ return # ユーザーがキャンセルした場合は何もしない
244
+ if not file_path:
245
+ file_path, _ = QFileDialog.getOpenFileName(
246
+ self, "Open MOL/SDF File", "",
247
+ "MOL/SDF Files (*.mol *.sdf);;All Files (*)"
248
+ )
249
+ if not file_path:
250
+ return
251
+
252
+ try:
253
+
254
+ # Determine extension early and handle .mol specially by reading the
255
+ # raw block and running it through fix_mol_block before parsing.
256
+ _, ext = os.path.splitext(file_path)
257
+ ext = ext.lower() if ext else ''
258
+
259
+ if ext == '.sdf':
260
+ suppl = Chem.SDMolSupplier(file_path, removeHs=False)
261
+ mol = next(suppl, None)
262
+
263
+ elif ext == '.mol':
264
+ # Read the file contents and attempt to fix malformed counts lines
265
+ with open(file_path, 'r', encoding='utf-8', errors='replace') as fh:
266
+ raw = fh.read()
267
+ fixed_block = self.fix_mol_block(raw)
268
+ mol = Chem.MolFromMolBlock(fixed_block, sanitize=True, removeHs=False)
269
+
270
+ # If parsing the fixed block fails, fall back to RDKit's file reader
271
+ # as a last resort (keeps behavior conservative).
272
+ if mol is None:
273
+ try:
274
+ mol = Chem.MolFromMolFile(file_path, removeHs=False)
275
+ except Exception:
276
+ mol = None
277
+
278
+ if mol is None:
279
+ self.statusBar().showMessage(f"Failed to load molecule from {file_path}")
280
+ return
281
+
282
+ else:
283
+ # Default: let RDKit try to read the file (most common case)
284
+ if file_path.lower().endswith('.sdf'):
285
+ suppl = Chem.SDMolSupplier(file_path, removeHs=False)
286
+ mol = next(suppl, None)
287
+ else:
288
+ mol = Chem.MolFromMolFile(file_path, removeHs=False)
289
+
290
+ if mol is None:
291
+ self.statusBar().showMessage(f"Failed to load molecule from {file_path}")
292
+ return
293
+
294
+ # 3D座標がない場合は2Dから3D変換(最適化なし)
295
+ if mol.GetNumConformers() == 0:
296
+ self.statusBar().showMessage("No 3D coordinates found. Converting to 3D...")
297
+ try:
298
+ try:
299
+ AllChem.EmbedMolecule(mol)
300
+ # 最適化は実行しない
301
+ # 3D変換直後にUndoスタックに積む
302
+ self.current_mol = mol
303
+ self.push_undo_state()
304
+ except Exception:
305
+ # If skipping chemistry checks, allow molecule to be displayed without 3D embedding
306
+ if self.settings.get('skip_chemistry_checks', False):
307
+ self.statusBar().showMessage("Warning: failed to generate 3D coordinates but skip_chemistry_checks is enabled; continuing.")
308
+ # Keep mol as-is (may lack conformer); downstream code checks for conformers
309
+ else:
310
+ raise
311
+ except:
312
+ self.statusBar().showMessage("Failed to generate 3D coordinates")
313
+ return
314
+
315
+ # Clear XYZ markers on the newly loaded MOL/SDF so Optimize 3D is
316
+ # correctly enabled when appropriate.
317
+ try:
318
+ self._clear_xyz_flags(mol)
319
+ except Exception:
320
+ pass
321
+
322
+ # 3Dビューアーに表示
323
+ # Centralized chemical/sanitization handling
324
+ # Ensure the skip_chemistry_checks setting is respected and flags are set
325
+ self._apply_chem_check_and_set_flags(mol, source_desc='MOL/SDF')
326
+
327
+ self.current_mol = mol
328
+ self.draw_molecule_3d(mol)
329
+
330
+ # カメラをリセット
331
+ self.plotter.reset_camera()
332
+
333
+ # UIを3Dビューアーモードに設定
334
+ self._enter_3d_viewer_ui_mode()
335
+
336
+ # メニューテキストと状態を更新
337
+ self.update_atom_id_menu_text()
338
+ self.update_atom_id_menu_state()
339
+
340
+ self.statusBar().showMessage(f"Loaded {file_path} in 3D viewer")
341
+
342
+ self.reset_undo_stack()
343
+ self.has_unsaved_changes = False # ファイル読込直後は未変更扱い
344
+ self.current_file_path = file_path
345
+ self.update_window_title()
346
+
347
+
348
+ except Exception as e:
349
+ self.statusBar().showMessage(f"Error loading MOL/SDF file: {e}")
350
+
@@ -0,0 +1,110 @@
1
+ from PyQt6.QtWidgets import (
2
+ QDialog, QVBoxLayout, QLabel, QButtonGroup, QRadioButton,
3
+ QHBoxLayout, QPushButton, QMessageBox
4
+ )
5
+ from rdkit import Chem
6
+
7
+ class MirrorDialog(QDialog):
8
+ """分子の鏡像を作成するダイアログ"""
9
+
10
+ def __init__(self, mol, main_window, parent=None):
11
+ super().__init__(parent)
12
+ self.mol = mol
13
+ self.main_window = main_window
14
+ self.init_ui()
15
+
16
+ def init_ui(self):
17
+ self.setWindowTitle("Mirror Molecule")
18
+ self.setMinimumSize(300, 200)
19
+
20
+ layout = QVBoxLayout(self)
21
+
22
+ # 説明テキスト
23
+ info_label = QLabel("Select the mirror plane to create molecular mirror image:")
24
+ layout.addWidget(info_label)
25
+
26
+ # ミラー平面選択のラジオボタン
27
+ self.plane_group = QButtonGroup(self)
28
+
29
+ self.xy_radio = QRadioButton("XY plane (Z = 0)")
30
+ self.xz_radio = QRadioButton("XZ plane (Y = 0)")
31
+ self.yz_radio = QRadioButton("YZ plane (X = 0)")
32
+
33
+ self.xy_radio.setChecked(True) # デフォルト選択
34
+
35
+ self.plane_group.addButton(self.xy_radio, 0)
36
+ self.plane_group.addButton(self.xz_radio, 1)
37
+ self.plane_group.addButton(self.yz_radio, 2)
38
+
39
+ layout.addWidget(self.xy_radio)
40
+ layout.addWidget(self.xz_radio)
41
+ layout.addWidget(self.yz_radio)
42
+
43
+ layout.addSpacing(20)
44
+
45
+ # ボタン
46
+ button_layout = QHBoxLayout()
47
+
48
+ apply_button = QPushButton("Apply Mirror")
49
+ apply_button.clicked.connect(self.apply_mirror)
50
+
51
+ close_button = QPushButton("Close")
52
+ close_button.clicked.connect(self.reject)
53
+
54
+ button_layout.addWidget(apply_button)
55
+ button_layout.addWidget(close_button)
56
+
57
+ layout.addLayout(button_layout)
58
+
59
+ def apply_mirror(self):
60
+ """選択された平面に対してミラー変換を適用"""
61
+ if not self.mol or self.mol.GetNumConformers() == 0:
62
+ QMessageBox.warning(self, "Error", "No 3D coordinates available.")
63
+ return
64
+
65
+ # 選択された平面を取得
66
+ plane_id = self.plane_group.checkedId()
67
+
68
+ try:
69
+ conf = self.mol.GetConformer()
70
+
71
+ # 各原子の座標を変換
72
+ for atom_idx in range(self.mol.GetNumAtoms()):
73
+ pos = conf.GetAtomPosition(atom_idx)
74
+
75
+ if plane_id == 0: # XY平面(Z軸に対してミラー)
76
+ new_pos = [pos.x, pos.y, -pos.z]
77
+ elif plane_id == 1: # XZ平面(Y軸に対してミラー)
78
+ new_pos = [pos.x, -pos.y, pos.z]
79
+ elif plane_id == 2: # YZ平面(X軸に対してミラー)
80
+ new_pos = [-pos.x, pos.y, pos.z]
81
+
82
+ # 新しい座標を設定
83
+ from rdkit.Geometry import Point3D
84
+ conf.SetAtomPosition(atom_idx, Point3D(new_pos[0], new_pos[1], new_pos[2]))
85
+
86
+ # 3Dビューを更新
87
+ self.main_window.draw_molecule_3d(self.mol)
88
+
89
+ # ミラー変換後にキラルタグを強制的に再計算
90
+ try:
91
+ if self.mol.GetNumConformers() > 0:
92
+ # 既存のキラルタグをクリア
93
+ for atom in self.mol.GetAtoms():
94
+ atom.SetChiralTag(Chem.rdchem.ChiralType.CHI_UNSPECIFIED)
95
+ # 3D座標から新しいキラルタグを計算
96
+ Chem.AssignAtomChiralTagsFromStructure(self.mol, confId=0)
97
+ except Exception as e:
98
+ print(f"Error updating chiral tags: {e}")
99
+
100
+ # キラルラベルを更新(鏡像変換でキラリティが変わる可能性があるため)
101
+ self.main_window.update_chiral_labels()
102
+
103
+ self.main_window.push_undo_state()
104
+
105
+ plane_names = ["XY", "XZ", "YZ"]
106
+ self.main_window.statusBar().showMessage(f"Molecule mirrored across {plane_names[plane_id]} plane.")
107
+
108
+
109
+ except Exception as e:
110
+ QMessageBox.critical(self, "Error", f"Failed to apply mirror transformation: {str(e)}")