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,454 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ main_window_dialog_manager.py
6
+ MainWindow (main_window.py) から分離されたモジュール
7
+ 機能クラス: MainWindowDialogManager
8
+ """
9
+
10
+
11
+ import os
12
+ import json
13
+
14
+
15
+ # RDKit imports (explicit to satisfy flake8 and used features)
16
+ try:
17
+ pass
18
+ except Exception:
19
+ pass
20
+
21
+ # PyQt6 Modules
22
+ from PyQt6.QtWidgets import (
23
+ QMessageBox,
24
+ QInputDialog
25
+ )
26
+
27
+
28
+
29
+ from PyQt6.QtCore import (
30
+ QDateTime
31
+ )
32
+
33
+
34
+ # Use centralized Open Babel availability from package-level __init__
35
+ # Use per-package modules availability (local __init__).
36
+ try:
37
+ from . import OBABEL_AVAILABLE
38
+ except Exception:
39
+ from modules import OBABEL_AVAILABLE
40
+ # Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
41
+ if OBABEL_AVAILABLE:
42
+ try:
43
+ from openbabel import pybel
44
+ except Exception:
45
+ # If import fails here, disable OBABEL locally; avoid raising
46
+ pybel = None
47
+ OBABEL_AVAILABLE = False
48
+ print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
49
+ else:
50
+ pybel = None
51
+
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
+ try:
58
+ import sip as _sip # type: ignore
59
+ _sip_isdeleted = getattr(_sip, 'isdeleted', None)
60
+ except Exception:
61
+ _sip = None
62
+ _sip_isdeleted = None
63
+
64
+ try:
65
+ # package relative imports (preferred when running as `python -m moleditpy`)
66
+ from .constants import VERSION
67
+ from .user_template_dialog import UserTemplateDialog
68
+ from .about_dialog import AboutDialog
69
+ from .translation_dialog import TranslationDialog
70
+ from .mirror_dialog import MirrorDialog
71
+ from .move_group_dialog import MoveGroupDialog
72
+ from .align_plane_dialog import AlignPlaneDialog
73
+ from .planarize_dialog import PlanarizeDialog
74
+ from .alignment_dialog import AlignmentDialog
75
+ from .periodic_table_dialog import PeriodicTableDialog
76
+ from .analysis_window import AnalysisWindow
77
+ from .bond_length_dialog import BondLengthDialog
78
+ from .angle_dialog import AngleDialog
79
+ from .dihedral_dialog import DihedralDialog
80
+ from .constrained_optimization_dialog import ConstrainedOptimizationDialog
81
+ except Exception:
82
+ # Fallback to absolute imports for script-style execution
83
+ from modules.constants import VERSION
84
+ from modules.user_template_dialog import UserTemplateDialog
85
+ from modules.about_dialog import AboutDialog
86
+ from modules.translation_dialog import TranslationDialog
87
+ from modules.mirror_dialog import MirrorDialog
88
+ from modules.move_group_dialog import MoveGroupDialog
89
+ from modules.align_plane_dialog import AlignPlaneDialog
90
+ from modules.planarize_dialog import PlanarizeDialog
91
+ from modules.alignment_dialog import AlignmentDialog
92
+ from modules.periodic_table_dialog import PeriodicTableDialog
93
+ from modules.analysis_window import AnalysisWindow
94
+ from modules.bond_length_dialog import BondLengthDialog
95
+ from modules.angle_dialog import AngleDialog
96
+ from modules.dihedral_dialog import DihedralDialog
97
+ from modules.constrained_optimization_dialog import ConstrainedOptimizationDialog
98
+
99
+
100
+ # --- クラス定義 ---
101
+ class MainWindowDialogManager(object):
102
+ """ main_window.py から分離された機能クラス """
103
+
104
+ def __init__(self, main_window):
105
+ """ クラスの初期化 """
106
+ self.mw = main_window
107
+
108
+
109
+ def show_about_dialog(self):
110
+ """Show the custom About dialog with Easter egg functionality"""
111
+ dialog = AboutDialog(self, self)
112
+ dialog.exec()
113
+
114
+
115
+
116
+ def open_periodic_table_dialog(self):
117
+ dialog=PeriodicTableDialog(self); dialog.element_selected.connect(self.set_atom_from_periodic_table)
118
+ checked_action=self.tool_group.checkedAction()
119
+ if checked_action: self.tool_group.setExclusive(False); checked_action.setChecked(False); self.tool_group.setExclusive(True)
120
+ dialog.exec()
121
+
122
+
123
+
124
+ def open_analysis_window(self):
125
+ if self.current_mol:
126
+ dialog = AnalysisWindow(self.current_mol, self, is_xyz_derived=self.is_xyz_derived)
127
+ dialog.exec()
128
+ else:
129
+ self.statusBar().showMessage("Please generate a 3D structure first to show analysis.")
130
+
131
+
132
+
133
+ def open_template_dialog(self):
134
+ """テンプレートダイアログを開く"""
135
+ dialog = UserTemplateDialog(self, self)
136
+ dialog.exec()
137
+
138
+
139
+
140
+ def open_template_dialog_and_activate(self):
141
+ """テンプレートダイアログを開き、テンプレートがメイン画面で使用できるようにする"""
142
+ # 既存のダイアログがあるかチェック
143
+ if hasattr(self, '_template_dialog') and self._template_dialog and not self._template_dialog.isHidden():
144
+ # 既存のダイアログを前面に表示
145
+ self._template_dialog.raise_()
146
+ self._template_dialog.activateWindow()
147
+ return
148
+
149
+ # 新しいダイアログを作成
150
+ self._template_dialog = UserTemplateDialog(self, self)
151
+ self._template_dialog.show() # モードレスで表示
152
+
153
+ # ダイアログが閉じられた後、テンプレートが選択されていればアクティブ化
154
+ def on_dialog_finished():
155
+ if hasattr(self._template_dialog, 'selected_template') and self._template_dialog.selected_template:
156
+ template_name = self._template_dialog.selected_template.get('name', 'user_template')
157
+ mode_name = f"template_user_{template_name}"
158
+
159
+ # Store template data for the scene to use
160
+ self.scene.user_template_data = self._template_dialog.selected_template
161
+ self.set_mode(mode_name)
162
+
163
+ # Update status
164
+ self.statusBar().showMessage(f"Template mode: {template_name}")
165
+
166
+ self._template_dialog.finished.connect(on_dialog_finished)
167
+
168
+
169
+
170
+ def save_2d_as_template(self):
171
+ """現在の2D構造をテンプレートとして保存"""
172
+ if not self.data.atoms:
173
+ QMessageBox.warning(self, "Warning", "No structure to save as template.")
174
+ return
175
+
176
+ # Get template name
177
+ name, ok = QInputDialog.getText(self, "Save Template", "Enter template name:")
178
+ if not ok or not name.strip():
179
+ return
180
+
181
+ name = name.strip()
182
+
183
+ try:
184
+ # Template directory
185
+ template_dir = os.path.join(self.settings_dir, 'user-templates')
186
+ if not os.path.exists(template_dir):
187
+ os.makedirs(template_dir)
188
+
189
+ # Convert current structure to template format
190
+ atoms_data = []
191
+ bonds_data = []
192
+
193
+ # Convert atoms
194
+ for atom_id, atom_info in self.data.atoms.items():
195
+ pos = atom_info['pos']
196
+ atoms_data.append({
197
+ 'id': atom_id,
198
+ 'symbol': atom_info['symbol'],
199
+ 'x': pos.x(),
200
+ 'y': pos.y(),
201
+ 'charge': atom_info.get('charge', 0),
202
+ 'radical': atom_info.get('radical', 0)
203
+ })
204
+
205
+ # Convert bonds
206
+ for (atom1_id, atom2_id), bond_info in self.data.bonds.items():
207
+ bonds_data.append({
208
+ 'atom1': atom1_id,
209
+ 'atom2': atom2_id,
210
+ 'order': bond_info['order'],
211
+ 'stereo': bond_info.get('stereo', 0)
212
+ })
213
+
214
+ # Create template data
215
+ template_data = {
216
+ 'format': "PME Template",
217
+ 'version': "1.0",
218
+ 'application': "MoleditPy",
219
+ 'application_version': VERSION,
220
+ 'name': name,
221
+ 'created': str(QDateTime.currentDateTime().toString()),
222
+ 'atoms': atoms_data,
223
+ 'bonds': bonds_data
224
+ }
225
+
226
+ # Save to file
227
+ filename = f"{name.replace(' ', '_')}.pmetmplt"
228
+ filepath = os.path.join(template_dir, filename)
229
+
230
+ if os.path.exists(filepath):
231
+ reply = QMessageBox.question(
232
+ self, "Overwrite Template",
233
+ f"Template '{name}' already exists. Overwrite?",
234
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
235
+ )
236
+ if reply != QMessageBox.StandardButton.Yes:
237
+ return
238
+
239
+ with open(filepath, 'w', encoding='utf-8') as f:
240
+ json.dump(template_data, f, indent=2, ensure_ascii=False)
241
+
242
+ # Mark as saved (no unsaved changes for this operation)
243
+ self.has_unsaved_changes = False
244
+ self.update_window_title()
245
+
246
+ QMessageBox.information(self, "Success", f"Template '{name}' saved successfully.")
247
+
248
+ except Exception as e:
249
+ QMessageBox.critical(self, "Error", f"Failed to save template: {str(e)}")
250
+
251
+
252
+
253
+ def open_translation_dialog(self):
254
+ """平行移動ダイアログを開く"""
255
+ # 測定モードを無効化
256
+ if self.measurement_mode:
257
+ self.measurement_action.setChecked(False)
258
+ self.toggle_measurement_mode(False)
259
+
260
+ dialog = TranslationDialog(self.current_mol, self)
261
+ self.active_3d_dialogs.append(dialog) # 参照を保持
262
+ dialog.show() # execではなくshowを使用してモードレス表示
263
+ dialog.accepted.connect(lambda: self.statusBar().showMessage("Translation applied."))
264
+ dialog.accepted.connect(self.push_undo_state)
265
+ dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog)) # ダイアログが閉じられた時にリストから削除
266
+
267
+
268
+
269
+ def open_move_group_dialog(self):
270
+ """Move Groupダイアログを開く"""
271
+ # 測定モードを無効化
272
+ if self.measurement_mode:
273
+ self.measurement_action.setChecked(False)
274
+ self.toggle_measurement_mode(False)
275
+
276
+ dialog = MoveGroupDialog(self.current_mol, self)
277
+ self.active_3d_dialogs.append(dialog)
278
+ dialog.show()
279
+ dialog.accepted.connect(lambda: self.statusBar().showMessage("Group transformation applied."))
280
+ dialog.accepted.connect(self.push_undo_state)
281
+ dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog))
282
+
283
+
284
+
285
+ def open_align_plane_dialog(self, plane):
286
+ """alignダイアログを開く"""
287
+ # 事前選択された原子を取得(測定モード無効化前に)
288
+ preselected_atoms = []
289
+ if hasattr(self, 'selected_atoms_3d') and self.selected_atoms_3d:
290
+ preselected_atoms = list(self.selected_atoms_3d)
291
+ elif hasattr(self, 'selected_atoms_for_measurement') and self.selected_atoms_for_measurement:
292
+ preselected_atoms = list(self.selected_atoms_for_measurement)
293
+
294
+ # 測定モードを無効化
295
+ if self.measurement_mode:
296
+ self.measurement_action.setChecked(False)
297
+ self.toggle_measurement_mode(False)
298
+
299
+ dialog = AlignPlaneDialog(self.current_mol, self, plane, preselected_atoms)
300
+ self.active_3d_dialogs.append(dialog) # 参照を保持
301
+ dialog.show() # execではなくshowを使用してモードレス表示
302
+ dialog.accepted.connect(lambda: self.statusBar().showMessage(f"Atoms alignd to {plane.upper()} plane."))
303
+ dialog.accepted.connect(self.push_undo_state)
304
+ dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog)) # ダイアログが閉じられた時にリストから削除
305
+
306
+
307
+
308
+ def open_planarize_dialog(self, plane=None):
309
+ """選択原子群を最適平面へ投影するダイアログを開く"""
310
+ # 事前選択された原子を取得(測定モード無効化前に)
311
+ preselected_atoms = []
312
+ if hasattr(self, 'selected_atoms_3d') and self.selected_atoms_3d:
313
+ preselected_atoms = list(self.selected_atoms_3d)
314
+ elif hasattr(self, 'selected_atoms_for_measurement') and self.selected_atoms_for_measurement:
315
+ preselected_atoms = list(self.selected_atoms_for_measurement)
316
+
317
+ # 測定モードを無効化
318
+ if self.measurement_mode:
319
+ self.measurement_action.setChecked(False)
320
+ self.toggle_measurement_mode(False)
321
+
322
+ dialog = PlanarizeDialog(self.current_mol, self, preselected_atoms)
323
+ self.active_3d_dialogs.append(dialog)
324
+ dialog.show()
325
+ dialog.accepted.connect(lambda: self.statusBar().showMessage("Selection planarized to best-fit plane."))
326
+ dialog.accepted.connect(self.push_undo_state)
327
+ dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog))
328
+
329
+
330
+
331
+ def open_alignment_dialog(self, axis):
332
+ """アライメントダイアログを開く"""
333
+ # 事前選択された原子を取得(測定モード無効化前に)
334
+ preselected_atoms = []
335
+ if hasattr(self, 'selected_atoms_3d') and self.selected_atoms_3d:
336
+ preselected_atoms = list(self.selected_atoms_3d)
337
+ elif hasattr(self, 'selected_atoms_for_measurement') and self.selected_atoms_for_measurement:
338
+ preselected_atoms = list(self.selected_atoms_for_measurement)
339
+
340
+ # 測定モードを無効化
341
+ if self.measurement_mode:
342
+ self.measurement_action.setChecked(False)
343
+ self.toggle_measurement_mode(False)
344
+
345
+ dialog = AlignmentDialog(self.current_mol, self, axis, preselected_atoms)
346
+ self.active_3d_dialogs.append(dialog) # 参照を保持
347
+ dialog.show() # execではなくshowを使用してモードレス表示
348
+ dialog.accepted.connect(lambda: self.statusBar().showMessage(f"Atoms aligned to {axis.upper()}-axis."))
349
+ dialog.accepted.connect(self.push_undo_state)
350
+ dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog)) # ダイアログが閉じられた時にリストから削除
351
+
352
+
353
+
354
+ def open_bond_length_dialog(self):
355
+ """結合長変換ダイアログを開く"""
356
+ # 事前選択された原子を取得(測定モード無効化前に)
357
+ preselected_atoms = []
358
+ if hasattr(self, 'selected_atoms_3d') and self.selected_atoms_3d:
359
+ preselected_atoms = list(self.selected_atoms_3d)
360
+ elif hasattr(self, 'selected_atoms_for_measurement') and self.selected_atoms_for_measurement:
361
+ preselected_atoms = list(self.selected_atoms_for_measurement)
362
+
363
+ # 測定モードを無効化
364
+ if self.measurement_mode:
365
+ self.measurement_action.setChecked(False)
366
+ self.toggle_measurement_mode(False)
367
+
368
+ dialog = BondLengthDialog(self.current_mol, self, preselected_atoms)
369
+ self.active_3d_dialogs.append(dialog) # 参照を保持
370
+ dialog.show() # execではなくshowを使用してモードレス表示
371
+ dialog.accepted.connect(lambda: self.statusBar().showMessage("Bond length adjusted."))
372
+ dialog.accepted.connect(self.push_undo_state)
373
+ dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog)) # ダイアログが閉じられた時にリストから削除
374
+
375
+
376
+
377
+ def open_angle_dialog(self):
378
+ """角度変換ダイアログを開く"""
379
+ # 事前選択された原子を取得(測定モード無効化前に)
380
+ preselected_atoms = []
381
+ if hasattr(self, 'selected_atoms_3d') and self.selected_atoms_3d:
382
+ preselected_atoms = list(self.selected_atoms_3d)
383
+ elif hasattr(self, 'selected_atoms_for_measurement') and self.selected_atoms_for_measurement:
384
+ preselected_atoms = list(self.selected_atoms_for_measurement)
385
+
386
+ # 測定モードを無効化
387
+ if self.measurement_mode:
388
+ self.measurement_action.setChecked(False)
389
+ self.toggle_measurement_mode(False)
390
+
391
+ dialog = AngleDialog(self.current_mol, self, preselected_atoms)
392
+ self.active_3d_dialogs.append(dialog) # 参照を保持
393
+ dialog.show() # execではなくshowを使用してモードレス表示
394
+ dialog.accepted.connect(lambda: self.statusBar().showMessage("Angle adjusted."))
395
+ dialog.accepted.connect(self.push_undo_state)
396
+ dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog)) # ダイアログが閉じられた時にリストから削除
397
+
398
+
399
+
400
+ def open_dihedral_dialog(self):
401
+ """二面角変換ダイアログを開く"""
402
+ # 事前選択された原子を取得(測定モード無効化前に)
403
+ preselected_atoms = []
404
+ if hasattr(self, 'selected_atoms_3d') and self.selected_atoms_3d:
405
+ preselected_atoms = list(self.selected_atoms_3d)
406
+ elif hasattr(self, 'selected_atoms_for_measurement') and self.selected_atoms_for_measurement:
407
+ preselected_atoms = list(self.selected_atoms_for_measurement)
408
+
409
+ # 測定モードを無効化
410
+ if self.measurement_mode:
411
+ self.measurement_action.setChecked(False)
412
+ self.toggle_measurement_mode(False)
413
+
414
+ dialog = DihedralDialog(self.current_mol, self, preselected_atoms)
415
+ self.active_3d_dialogs.append(dialog) # 参照を保持
416
+ dialog.show() # execではなくshowを使用してモードレス表示
417
+ dialog.accepted.connect(lambda: self.statusBar().showMessage("Dihedral angle adjusted."))
418
+ dialog.accepted.connect(self.push_undo_state)
419
+ dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog)) # ダイアログが閉じられた時にリストから削除
420
+
421
+
422
+
423
+ def open_mirror_dialog(self):
424
+ """ミラー機能ダイアログを開く"""
425
+ if not self.current_mol:
426
+ self.statusBar().showMessage("No 3D molecule loaded.")
427
+ return
428
+
429
+ # 測定モードを無効化
430
+ if self.measurement_mode:
431
+ self.measurement_action.setChecked(False)
432
+ self.toggle_measurement_mode(False)
433
+
434
+ dialog = MirrorDialog(self.current_mol, self)
435
+ dialog.exec() # モーダルダイアログとして表示
436
+
437
+
438
+
439
+ def open_constrained_optimization_dialog(self):
440
+ """制約付き最適化ダイアログを開く"""
441
+ if not self.current_mol:
442
+ self.statusBar().showMessage("No 3D molecule loaded.")
443
+ return
444
+
445
+ # 測定モードを無効化
446
+ if self.measurement_mode:
447
+ self.measurement_action.setChecked(False)
448
+ self.toggle_measurement_mode(False)
449
+
450
+ dialog = ConstrainedOptimizationDialog(self.current_mol, self)
451
+ self.active_3d_dialogs.append(dialog) # 参照を保持
452
+ dialog.show() # モードレス表示
453
+ dialog.finished.connect(lambda: self.remove_dialog_from_list(dialog))
454
+