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