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,434 @@
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_project_io.py
15
+ MainWindow (main_window.py) から分離されたモジュール
16
+ 機能クラス: MainWindowProjectIo
17
+ """
18
+
19
+
20
+ import pickle
21
+ import os
22
+ import json
23
+ import traceback
24
+
25
+
26
+ # RDKit imports (explicit to satisfy flake8 and used features)
27
+ try:
28
+ pass
29
+ except Exception:
30
+ pass
31
+
32
+ # PyQt6 Modules
33
+ from PyQt6.QtWidgets import (
34
+ QFileDialog, QMessageBox
35
+ )
36
+
37
+
38
+
39
+ from PyQt6.QtCore import (
40
+ QTimer
41
+ )
42
+
43
+
44
+ # Use centralized Open Babel availability from package-level __init__
45
+ # Use per-package modules availability (local __init__).
46
+ try:
47
+ from . import OBABEL_AVAILABLE
48
+ except Exception:
49
+ from modules import OBABEL_AVAILABLE
50
+ # Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
51
+ if OBABEL_AVAILABLE:
52
+ try:
53
+ from openbabel import pybel
54
+ except Exception:
55
+ # If import fails here, disable OBABEL locally; avoid raising
56
+ pybel = None
57
+ OBABEL_AVAILABLE = False
58
+ print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
59
+ else:
60
+ pybel = None
61
+
62
+ # Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
63
+ # allows safely detecting C++ wrapper objects that have been deleted. Import
64
+ # it once at module import time and expose a small, robust wrapper so callers
65
+ # can avoid re-importing sip repeatedly and so we centralize exception
66
+ # handling (this reduces crash risk during teardown and deletion operations).
67
+ try:
68
+ import sip as _sip # type: ignore
69
+ _sip_isdeleted = getattr(_sip, 'isdeleted', None)
70
+ except Exception:
71
+ _sip = None
72
+ _sip_isdeleted = None
73
+
74
+ try:
75
+ # package relative imports (preferred when running as `python -m moleditpy`)
76
+ pass
77
+ except Exception:
78
+ # Fallback to absolute imports for script-style execution
79
+ pass
80
+
81
+
82
+ # --- クラス定義 ---
83
+ class MainWindowProjectIo(object):
84
+ """ main_window.py から分離された機能クラス """
85
+
86
+
87
+ def save_project(self):
88
+ """上書き保存(Ctrl+S)- デフォルトでPMEPRJ形式"""
89
+ if not self.data.atoms and not self.current_mol:
90
+ self.statusBar().showMessage("Error: Nothing to save.")
91
+ return
92
+ # 非ネイティブ形式(.mol, .sdf, .xyz など)は上書き保存せず、必ず「名前を付けて保存」にする
93
+ native_exts = ['.pmeprj', '.pmeraw']
94
+ if self.current_file_path and any(self.current_file_path.lower().endswith(ext) for ext in native_exts):
95
+ # 既存のPMEPRJ/PMERAWファイルの場合は上書き保存
96
+ try:
97
+ if self.current_file_path.lower().endswith('.pmeraw'):
98
+ # 既存のPMERAWファイルの場合はPMERAW形式で保存
99
+ save_data = self.get_current_state()
100
+ with open(self.current_file_path, 'wb') as f:
101
+ pickle.dump(save_data, f)
102
+ else:
103
+ # PMEPRJ形式で保存
104
+ json_data = self.create_json_data()
105
+ with open(self.current_file_path, 'w', encoding='utf-8') as f:
106
+ json.dump(json_data, f, indent=2, ensure_ascii=False)
107
+
108
+ # 保存成功時に状態をリセット
109
+ self.has_unsaved_changes = False
110
+ self.update_window_title()
111
+
112
+ self.statusBar().showMessage(f"Project saved to {self.current_file_path}")
113
+
114
+ except (OSError, IOError) as e:
115
+ self.statusBar().showMessage(f"File I/O error: {e}")
116
+ except (pickle.PicklingError, TypeError, ValueError) as e:
117
+ self.statusBar().showMessage(f"Data serialization error: {e}")
118
+ except Exception as e:
119
+ self.statusBar().showMessage(f"Error saving project file: {e}")
120
+
121
+ traceback.print_exc()
122
+ else:
123
+ # MOL/SDF/XYZなどは上書き保存せず、必ず「名前を付けて保存」にする
124
+ self.save_project_as()
125
+
126
+
127
+
128
+ def save_project_as(self):
129
+ """名前を付けて保存(Ctrl+Shift+S)- デフォルトでPMEPRJ形式"""
130
+ if not self.data.atoms and not self.current_mol:
131
+ self.statusBar().showMessage("Error: Nothing to save.")
132
+ return
133
+
134
+ try:
135
+ # Determine a sensible default filename based on current file (strip extension)
136
+ default_name = "untitled"
137
+ try:
138
+ if self.current_file_path:
139
+ base = os.path.basename(self.current_file_path)
140
+ default_name = os.path.splitext(base)[0]
141
+ except Exception:
142
+ default_name = "untitled"
143
+
144
+ # Prefer the directory of the currently opened file as default
145
+ default_path = default_name
146
+ try:
147
+ if self.current_file_path:
148
+ default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
149
+ except Exception:
150
+ default_path = default_name
151
+
152
+ file_path, _ = QFileDialog.getSaveFileName(
153
+ self, "Save Project As", default_path,
154
+ "PME Project Files (*.pmeprj);;All Files (*)",
155
+ )
156
+ if not file_path:
157
+ return
158
+
159
+ if not file_path.lower().endswith('.pmeprj'):
160
+ file_path += '.pmeprj'
161
+
162
+ # JSONデータを保存
163
+ json_data = self.create_json_data()
164
+ with open(file_path, 'w', encoding='utf-8') as f:
165
+ json.dump(json_data, f, indent=2, ensure_ascii=False)
166
+
167
+ # 保存成功時に状態をリセット
168
+ self.has_unsaved_changes = False
169
+ # Replace current file with the newly saved file so subsequent saves go to this path
170
+ self.current_file_path = file_path
171
+ self.update_window_title()
172
+ # Mark this state as the last saved state for undo tracking
173
+ try:
174
+ self._saved_state = copy.deepcopy(self.get_current_state())
175
+ except Exception:
176
+ pass
177
+
178
+ self.statusBar().showMessage(f"Project saved to {file_path}")
179
+
180
+ except (OSError, IOError) as e:
181
+ self.statusBar().showMessage(f"File I/O error: {e}")
182
+ except pickle.PicklingError as e:
183
+ self.statusBar().showMessage(f"Data serialization error: {e}")
184
+ except Exception as e:
185
+ self.statusBar().showMessage(f"Error saving project file: {e}")
186
+
187
+ traceback.print_exc()
188
+
189
+
190
+
191
+ def save_raw_data(self):
192
+ if not self.data.atoms and not self.current_mol:
193
+ self.statusBar().showMessage("Error: Nothing to save.")
194
+ return
195
+
196
+ try:
197
+ save_data = self.get_current_state()
198
+ # default filename based on current file
199
+ default_name = "untitled"
200
+ try:
201
+ if self.current_file_path:
202
+ base = os.path.basename(self.current_file_path)
203
+ default_name = os.path.splitext(base)[0]
204
+ except Exception:
205
+ default_name = "untitled"
206
+
207
+ # prefer same directory as current file when available
208
+ default_path = default_name
209
+ try:
210
+ if self.current_file_path:
211
+ default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
212
+ except Exception:
213
+ default_path = default_name
214
+
215
+ file_path, _ = QFileDialog.getSaveFileName(self, "Save Project File", default_path, "Project Files (*.pmeraw);;All Files (*)")
216
+ if not file_path:
217
+ return
218
+
219
+ if not file_path.lower().endswith('.pmeraw'):
220
+ file_path += '.pmeraw'
221
+
222
+ with open(file_path, 'wb') as f:
223
+ pickle.dump(save_data, f)
224
+
225
+ # 保存成功時に状態をリセット
226
+ self.has_unsaved_changes = False
227
+ # Update current file to the newly saved raw file
228
+ self.current_file_path = file_path
229
+ self.update_window_title()
230
+ try:
231
+ self._saved_state = copy.deepcopy(self.get_current_state())
232
+ except Exception:
233
+ pass
234
+
235
+ self.statusBar().showMessage(f"Project saved to {file_path}")
236
+
237
+ except (OSError, IOError) as e:
238
+ self.statusBar().showMessage(f"File I/O error: {e}")
239
+ except pickle.PicklingError as e:
240
+ self.statusBar().showMessage(f"Data serialization error: {e}")
241
+ except Exception as e:
242
+ self.statusBar().showMessage(f"Error saving project file: {e}")
243
+
244
+ traceback.print_exc()
245
+
246
+
247
+
248
+
249
+ def load_raw_data(self, file_path=None):
250
+ if not file_path:
251
+ file_path, _ = QFileDialog.getOpenFileName(self, "Open Project File", "", "Project Files (*.pmeraw);;All Files (*)")
252
+ if not file_path:
253
+ return
254
+
255
+ try:
256
+ with open(file_path, 'rb') as f:
257
+ loaded_data = pickle.load(f)
258
+ self.restore_ui_for_editing()
259
+ self.set_state_from_data(loaded_data)
260
+
261
+ # ファイル読み込み時に状態をリセット
262
+ self.reset_undo_stack()
263
+ self.has_unsaved_changes = False
264
+ self.current_file_path = file_path
265
+ self.update_window_title()
266
+ try:
267
+ self._saved_state= copy.deepcopy(self.et_current_state())
268
+ except Exception:
269
+ pass
270
+
271
+ self.statusBar.showMessage(f"Project loaded from {file_path}")
272
+
273
+ QTimer.singleShot(0, self.fit_to_view)
274
+
275
+ except FileNotFoundError:
276
+ self.statusBar().showMessage(f"File not found: {file_path}")
277
+ except (OSError, IOError) as e:
278
+ self.statusBar().showMessage(f"File I/O error: {e}")
279
+ except pickle.UnpicklingError as e:
280
+ self.statusBar().showMessage(f"Invalid project file format: {e}")
281
+ except Exception as e:
282
+ self.statusBar().showMessage(f"Error loading project file: {e}")
283
+
284
+ traceback.print_exc()
285
+
286
+
287
+
288
+ def save_as_json(self):
289
+ """PMEJSONファイル形式で保存 (3D MOL情報含む)"""
290
+ if not self.data.atoms and not self.current_mol:
291
+ self.statusBar().showMessage("Error: Nothing to save.")
292
+ return
293
+
294
+ try:
295
+ # default filename based on current file
296
+ default_name = "untitled"
297
+ try:
298
+ if self.current_file_path:
299
+ base = os.path.basename(self.current_file_path)
300
+ default_name = os.path.splitext(base)[0]
301
+ except Exception:
302
+ default_name = "untitled"
303
+
304
+ # prefer same directory as current file when available
305
+ default_path = default_name
306
+ try:
307
+ if self.current_file_path:
308
+ default_path = os.path.join(os.path.dirname(self.current_file_path), default_name)
309
+ except Exception:
310
+ default_path = default_name
311
+
312
+ file_path, _ = QFileDialog.getSaveFileName(
313
+ self, "Save as PME Project", default_path,
314
+ "PME Project Files (*.pmeprj);;All Files (*)",
315
+ )
316
+ if not file_path:
317
+ return
318
+
319
+ if not file_path.lower().endswith('.pmeprj'):
320
+ file_path += '.pmeprj'
321
+
322
+ # JSONデータを作成
323
+ json_data = self.create_json_data()
324
+
325
+ # JSON形式で保存(美しい整形付き)
326
+ with open(file_path, 'w', encoding='utf-8') as f:
327
+ json.dump(json_data, f, indent=2, ensure_ascii=False)
328
+
329
+ # 保存成功時に状態をリセット
330
+ self.has_unsaved_changes = False
331
+ # Replace current file with the newly saved PME Project
332
+ self.current_file_path = file_path
333
+ self.update_window_title()
334
+
335
+ self.statusBar().showMessage(f"PME Project saved to {file_path}")
336
+
337
+ except (OSError, IOError) as e:
338
+ self.statusBar().showMessage(f"File I/O error: {e}")
339
+ except (TypeError, ValueError) as e:
340
+ self.statusBar().showMessage(f"JSON serialization error: {e}")
341
+ except Exception as e:
342
+ self.statusBar().showMessage(f"Error saving PME Project file: {e}")
343
+
344
+ traceback.print_exc()
345
+
346
+
347
+
348
+ def load_json_data(self, file_path=None):
349
+ """PME Projectファイル形式を読み込み"""
350
+ if not file_path:
351
+ file_path, _ = QFileDialog.getOpenFileName(
352
+ self, "Open PME Project File", "",
353
+ "PME Project Files (*.pmeprj);;All Files (*)",
354
+ )
355
+ if not file_path:
356
+ return
357
+
358
+ try:
359
+ with open(file_path, 'r', encoding='utf-8') as f:
360
+ json_data = json.load(f)
361
+
362
+ # フォーマット検証
363
+ if json_data.get("format") != "PME Project":
364
+ QMessageBox.warning(
365
+ self, "Invalid Format",
366
+ "This file is not a valid PME Project format."
367
+ )
368
+ return
369
+
370
+ # バージョン確認
371
+ file_version = json_data.get("version", "1.0")
372
+ if file_version != "1.0":
373
+ QMessageBox.information(
374
+ self, "Version Notice",
375
+ f"This file was created with PME Project version {file_version}.\n"
376
+ "Loading will be attempted but some features may not work correctly."
377
+ )
378
+
379
+ self.restore_ui_for_editing()
380
+ self.load_from_json_data(json_data)
381
+ # ファイル読み込み時に状態をリセット
382
+ self.reset_undo_stack()
383
+ self.has_unsaved_changes = False
384
+ self.current_file_path = file_path
385
+ self.update_window_title()
386
+
387
+ self.statusBar().showMessage(f"PME Project loaded from {file_path}")
388
+
389
+ QTimer.singleShot(0, self.fit_to_view)
390
+
391
+ except FileNotFoundError:
392
+ self.statusBar().showMessage(f"File not found: {file_path}")
393
+ except json.JSONDecodeError as e:
394
+ self.statusBar().showMessage(f"Invalid JSON format: {e}")
395
+ except (OSError, IOError) as e:
396
+ self.statusBar().showMessage(f"File I/O error: {e}")
397
+ except Exception as e:
398
+ self.statusBar().showMessage(f"Error loading PME Project file: {e}")
399
+
400
+ traceback.print_exc()
401
+
402
+
403
+
404
+ def open_project_file(self, file_path=None):
405
+ """プロジェクトファイルを開く(.pmeprjと.pmerawの両方に対応)"""
406
+ # Check for unsaved changes before opening a new project file.
407
+ # Previously this function opened .pmeprj/.pmeraw without prompting the
408
+ # user to save current unsaved work. Ensure we honor the global
409
+ # unsaved-change check like other loaders (SMILES/MOL/etc.).
410
+ if not self.check_unsaved_changes():
411
+ return
412
+ if not file_path:
413
+ file_path, _ = QFileDialog.getOpenFileName(
414
+ self, "Open Project File", "",
415
+ "PME Project Files (*.pmeprj);;PME Raw Files (*.pmeraw);;All Files (*)",
416
+ )
417
+ if not file_path:
418
+ return
419
+
420
+ # 拡張子に応じて適切な読み込み関数を呼び出し
421
+ if file_path.lower().endswith('.pmeprj'):
422
+ self.load_json_data(file_path)
423
+ elif file_path.lower().endswith('.pmeraw'):
424
+ self.load_raw_data(file_path)
425
+ else:
426
+ # 拡張子不明の場合はJSONとして試行
427
+ try:
428
+ self.load_json_data(file_path)
429
+ except Exception:
430
+ try:
431
+ self.load_raw_data(file_path)
432
+ except Exception:
433
+ self.statusBar().showMessage("Error: Unable to determine file format.")
434
+