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.
- moleditpy_linux/__init__.py +17 -0
- moleditpy_linux/__main__.py +29 -0
- moleditpy_linux/main.py +37 -0
- moleditpy_linux/modules/__init__.py +41 -0
- moleditpy_linux/modules/about_dialog.py +104 -0
- moleditpy_linux/modules/align_plane_dialog.py +292 -0
- moleditpy_linux/modules/alignment_dialog.py +272 -0
- moleditpy_linux/modules/analysis_window.py +209 -0
- moleditpy_linux/modules/angle_dialog.py +440 -0
- moleditpy_linux/modules/assets/file_icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.icns +0 -0
- moleditpy_linux/modules/assets/icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.png +0 -0
- moleditpy_linux/modules/atom_item.py +395 -0
- moleditpy_linux/modules/bond_item.py +464 -0
- moleditpy_linux/modules/bond_length_dialog.py +380 -0
- moleditpy_linux/modules/calculation_worker.py +766 -0
- moleditpy_linux/modules/color_settings_dialog.py +321 -0
- moleditpy_linux/modules/constants.py +88 -0
- moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
- moleditpy_linux/modules/custom_interactor_style.py +749 -0
- moleditpy_linux/modules/custom_qt_interactor.py +102 -0
- moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
- moleditpy_linux/modules/dihedral_dialog.py +443 -0
- moleditpy_linux/modules/main_window.py +850 -0
- moleditpy_linux/modules/main_window_app_state.py +787 -0
- moleditpy_linux/modules/main_window_compute.py +1242 -0
- moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
- moleditpy_linux/modules/main_window_edit_3d.py +536 -0
- moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
- moleditpy_linux/modules/main_window_export.py +917 -0
- moleditpy_linux/modules/main_window_main_init.py +2100 -0
- moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
- moleditpy_linux/modules/main_window_project_io.py +434 -0
- moleditpy_linux/modules/main_window_string_importers.py +275 -0
- moleditpy_linux/modules/main_window_ui_manager.py +602 -0
- moleditpy_linux/modules/main_window_view_3d.py +1539 -0
- moleditpy_linux/modules/main_window_view_loaders.py +355 -0
- moleditpy_linux/modules/mirror_dialog.py +122 -0
- moleditpy_linux/modules/molecular_data.py +302 -0
- moleditpy_linux/modules/molecule_scene.py +2000 -0
- moleditpy_linux/modules/move_group_dialog.py +600 -0
- moleditpy_linux/modules/periodic_table_dialog.py +84 -0
- moleditpy_linux/modules/planarize_dialog.py +220 -0
- moleditpy_linux/modules/plugin_interface.py +215 -0
- moleditpy_linux/modules/plugin_manager.py +473 -0
- moleditpy_linux/modules/plugin_manager_window.py +274 -0
- moleditpy_linux/modules/settings_dialog.py +1503 -0
- moleditpy_linux/modules/template_preview_item.py +157 -0
- moleditpy_linux/modules/template_preview_view.py +74 -0
- moleditpy_linux/modules/translation_dialog.py +364 -0
- moleditpy_linux/modules/user_template_dialog.py +692 -0
- moleditpy_linux/modules/zoomable_view.py +129 -0
- moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
- moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
- moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
- moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
- moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
- moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,602 @@
|
|
|
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_ui_manager.py
|
|
15
|
+
MainWindow (main_window.py) から分離されたモジュール
|
|
16
|
+
機能クラス: MainWindowUiManager
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
import vtk
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# RDKit imports (explicit to satisfy flake8 and used features)
|
|
24
|
+
try:
|
|
25
|
+
pass
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
# PyQt6 Modules
|
|
30
|
+
from PyQt6.QtWidgets import (
|
|
31
|
+
QApplication, QMainWindow, QGraphicsView, QDialog, QMessageBox
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
from PyQt6.QtCore import (
|
|
37
|
+
Qt, QEvent,
|
|
38
|
+
QTimer
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Use centralized Open Babel availability from package-level __init__
|
|
43
|
+
# Use per-package modules availability (local __init__).
|
|
44
|
+
try:
|
|
45
|
+
from . import OBABEL_AVAILABLE
|
|
46
|
+
except Exception:
|
|
47
|
+
from modules import OBABEL_AVAILABLE
|
|
48
|
+
# Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
|
|
49
|
+
if OBABEL_AVAILABLE:
|
|
50
|
+
try:
|
|
51
|
+
from openbabel import pybel
|
|
52
|
+
except Exception:
|
|
53
|
+
# If import fails here, disable OBABEL locally; avoid raising
|
|
54
|
+
pybel = None
|
|
55
|
+
OBABEL_AVAILABLE = False
|
|
56
|
+
print("Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled.")
|
|
57
|
+
else:
|
|
58
|
+
pybel = None
|
|
59
|
+
|
|
60
|
+
# Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
|
|
61
|
+
# allows safely detecting C++ wrapper objects that have been deleted. Import
|
|
62
|
+
# it once at module import time and expose a small, robust wrapper so callers
|
|
63
|
+
# can avoid re-importing sip repeatedly and so we centralize exception
|
|
64
|
+
# handling (this reduces crash risk during teardown and deletion operations).
|
|
65
|
+
try:
|
|
66
|
+
import sip as _sip # type: ignore
|
|
67
|
+
_sip_isdeleted = getattr(_sip, 'isdeleted', None)
|
|
68
|
+
except Exception:
|
|
69
|
+
_sip = None
|
|
70
|
+
_sip_isdeleted = None
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# package relative imports (preferred when running as `python -m moleditpy`)
|
|
74
|
+
from .custom_interactor_style import CustomInteractorStyle
|
|
75
|
+
except Exception:
|
|
76
|
+
# Fallback to absolute imports for script-style execution
|
|
77
|
+
from modules.custom_interactor_style import CustomInteractorStyle
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# --- クラス定義 ---
|
|
81
|
+
class MainWindowUiManager(object):
|
|
82
|
+
""" main_window.py から分離された機能クラス """
|
|
83
|
+
|
|
84
|
+
def __init__(self, main_window):
|
|
85
|
+
""" クラスの初期化 """
|
|
86
|
+
self = main_window
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def update_status_bar(self, message):
|
|
90
|
+
"""ワーカースレッドからのメッセージでステータスバーを更新するスロット"""
|
|
91
|
+
self.statusBar().showMessage(message)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def set_mode(self, mode_str):
|
|
96
|
+
prev_mode = getattr(self.scene, 'mode', None)
|
|
97
|
+
self.scene.mode = mode_str
|
|
98
|
+
self.view_2d.setMouseTracking(True)
|
|
99
|
+
# テンプレートモードから離れる場合はゴーストを消す
|
|
100
|
+
if prev_mode and prev_mode.startswith('template') and not mode_str.startswith('template'):
|
|
101
|
+
self.scene.clear_template_preview()
|
|
102
|
+
elif not mode_str.startswith('template'):
|
|
103
|
+
self.scene.template_preview.hide()
|
|
104
|
+
|
|
105
|
+
# カーソル形状の設定
|
|
106
|
+
if mode_str == 'select':
|
|
107
|
+
self.view_2d.setCursor(Qt.CursorShape.ArrowCursor)
|
|
108
|
+
elif mode_str.startswith(('atom', 'bond', 'template')):
|
|
109
|
+
self.view_2d.setCursor(Qt.CursorShape.CrossCursor)
|
|
110
|
+
elif mode_str.startswith(('charge', 'radical')):
|
|
111
|
+
self.view_2d.setCursor(Qt.CursorShape.CrossCursor)
|
|
112
|
+
else:
|
|
113
|
+
self.view_2d.setCursor(Qt.CursorShape.ArrowCursor)
|
|
114
|
+
|
|
115
|
+
if mode_str.startswith('atom'):
|
|
116
|
+
self.scene.current_atom_symbol = mode_str.split('_')[1]
|
|
117
|
+
self.statusBar().showMessage(f"Mode: Draw Atom ({self.scene.current_atom_symbol})")
|
|
118
|
+
self.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
119
|
+
self.view_2d.setMouseTracking(True)
|
|
120
|
+
self.scene.bond_order = 1
|
|
121
|
+
self.scene.bond_stereo = 0
|
|
122
|
+
elif mode_str.startswith('bond'):
|
|
123
|
+
self.scene.current_atom_symbol = 'C'
|
|
124
|
+
parts = mode_str.split('_')
|
|
125
|
+
self.scene.bond_order = int(parts[1])
|
|
126
|
+
self.scene.bond_stereo = int(parts[2]) if len(parts) > 2 else 0
|
|
127
|
+
stereo_text = {0: "", 1: " (Wedge)", 2: " (Dash)"}.get(self.scene.bond_stereo, "")
|
|
128
|
+
self.statusBar().showMessage(f"Mode: Draw Bond (Order: {self.scene.bond_order}{stereo_text})")
|
|
129
|
+
self.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
130
|
+
self.view_2d.setMouseTracking(True)
|
|
131
|
+
elif mode_str.startswith('template'):
|
|
132
|
+
if mode_str.startswith('template_user'):
|
|
133
|
+
# User template mode
|
|
134
|
+
template_name = mode_str.replace('template_user_', '')
|
|
135
|
+
self.statusBar().showMessage(f"Mode: User Template ({template_name})")
|
|
136
|
+
else:
|
|
137
|
+
# Built-in template mode
|
|
138
|
+
self.statusBar().showMessage(f"Mode: {mode_str.split('_')[1].capitalize()} Template")
|
|
139
|
+
self.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
140
|
+
elif mode_str == 'charge_plus':
|
|
141
|
+
self.statusBar().showMessage("Mode: Increase Charge (Click on Atom)")
|
|
142
|
+
self.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
143
|
+
elif mode_str == 'charge_minus':
|
|
144
|
+
self.statusBar().showMessage("Mode: Decrease Charge (Click on Atom)")
|
|
145
|
+
self.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
146
|
+
elif mode_str == 'radical':
|
|
147
|
+
self.statusBar().showMessage("Mode: Toggle Radical (Click on Atom)")
|
|
148
|
+
self.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
149
|
+
|
|
150
|
+
else: # Select mode
|
|
151
|
+
self.statusBar().showMessage("Mode: Select")
|
|
152
|
+
self.view_2d.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
|
153
|
+
self.scene.bond_order = 1
|
|
154
|
+
self.scene.bond_stereo = 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def set_mode_and_update_toolbar(self, mode_str):
|
|
159
|
+
self.set_mode(mode_str)
|
|
160
|
+
# QAction→QToolButtonのマッピングを取得
|
|
161
|
+
toolbar = getattr(self, 'toolbar', None)
|
|
162
|
+
action_to_button = {}
|
|
163
|
+
if toolbar:
|
|
164
|
+
for key, action in self.mode_actions.items():
|
|
165
|
+
btn = toolbar.widgetForAction(action)
|
|
166
|
+
if btn:
|
|
167
|
+
action_to_button[action] = btn
|
|
168
|
+
|
|
169
|
+
# すべてのモードボタンの選択解除&色リセット
|
|
170
|
+
for key, action in self.mode_actions.items():
|
|
171
|
+
action.setChecked(False)
|
|
172
|
+
btn = action_to_button.get(action)
|
|
173
|
+
if btn:
|
|
174
|
+
btn.setStyleSheet("")
|
|
175
|
+
|
|
176
|
+
# テンプレート系(User含む)は全て同じスタイル適用
|
|
177
|
+
if mode_str in self.mode_actions:
|
|
178
|
+
action = self.mode_actions[mode_str]
|
|
179
|
+
action.setChecked(True)
|
|
180
|
+
btn = action_to_button.get(action)
|
|
181
|
+
if btn:
|
|
182
|
+
# テンプレート系は青、それ以外はクリア
|
|
183
|
+
if mode_str.startswith('template'):
|
|
184
|
+
btn.setStyleSheet("background-color: #2196F3; color: white;")
|
|
185
|
+
else:
|
|
186
|
+
btn.setStyleSheet("")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def activate_select_mode(self):
|
|
191
|
+
self.set_mode('select')
|
|
192
|
+
if 'select' in self.mode_actions:
|
|
193
|
+
self.mode_actions['select'].setChecked(True)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def eventFilter(self, obj, event):
|
|
199
|
+
if obj is self.plotter and event.type() == QEvent.Type.MouseButtonPress:
|
|
200
|
+
self.view_2d.setFocus()
|
|
201
|
+
return super().eventFilter(obj, event)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def closeEvent(self, event):
|
|
206
|
+
# Persist settings on exit only when explicitly modified (deferred save)
|
|
207
|
+
try:
|
|
208
|
+
if getattr(self, 'settings_dirty', False) or self.settings != self.initial_settings:
|
|
209
|
+
self.save_settings()
|
|
210
|
+
self.settings_dirty = False
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# 未保存の変更がある場合の処理
|
|
215
|
+
if self.has_unsaved_changes:
|
|
216
|
+
reply = QMessageBox.question(
|
|
217
|
+
self, "Unsaved Changes",
|
|
218
|
+
"You have unsaved changes. Do you want to save them?",
|
|
219
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel,
|
|
220
|
+
QMessageBox.StandardButton.Yes
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
224
|
+
# 保存処理
|
|
225
|
+
self.save_project()
|
|
226
|
+
|
|
227
|
+
# 保存がキャンセルされた場合は終了もキャンセル
|
|
228
|
+
if self.has_unsaved_changes:
|
|
229
|
+
event.ignore()
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
elif reply == QMessageBox.StandardButton.Cancel:
|
|
233
|
+
event.ignore()
|
|
234
|
+
return
|
|
235
|
+
# No の場合はそのまま終了処理へ
|
|
236
|
+
|
|
237
|
+
# 開いているすべてのダイアログウィンドウを閉じる
|
|
238
|
+
try:
|
|
239
|
+
for widget in QApplication.topLevelWidgets():
|
|
240
|
+
if widget != self and isinstance(widget, (QDialog, QMainWindow)):
|
|
241
|
+
try:
|
|
242
|
+
widget.close()
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
# 終了処理
|
|
249
|
+
if self.scene and self.scene.template_preview:
|
|
250
|
+
self.scene.template_preview.hide()
|
|
251
|
+
|
|
252
|
+
# Clean up any active per-run calculation threads we spawned.
|
|
253
|
+
try:
|
|
254
|
+
for thr in list(getattr(self, '_active_calc_threads', []) or []):
|
|
255
|
+
try:
|
|
256
|
+
thr.quit()
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
259
|
+
try:
|
|
260
|
+
thr.wait(200)
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
event.accept()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def toggle_3d_edit_mode(self, checked):
|
|
271
|
+
"""「3D Drag」ボタンの状態に応じて編集モードを切り替える"""
|
|
272
|
+
if checked:
|
|
273
|
+
# 3D Editモードをオンにする時は、Measurementモードを無効化
|
|
274
|
+
if self.measurement_mode:
|
|
275
|
+
self.measurement_action.setChecked(False)
|
|
276
|
+
self.toggle_measurement_mode(False)
|
|
277
|
+
|
|
278
|
+
self.is_3d_edit_mode = checked
|
|
279
|
+
if checked:
|
|
280
|
+
self.statusBar().showMessage("3D Drag Mode: ON.")
|
|
281
|
+
else:
|
|
282
|
+
self.statusBar().showMessage("3D Drag Mode: OFF.")
|
|
283
|
+
self.view_2d.setFocus()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _setup_3d_picker(self):
|
|
288
|
+
self.plotter.picker = vtk.vtkCellPicker()
|
|
289
|
+
self.plotter.picker.SetTolerance(0.025)
|
|
290
|
+
|
|
291
|
+
# 新しいカスタムスタイル(原子移動用)のインスタンスを作成
|
|
292
|
+
style = CustomInteractorStyle(self)
|
|
293
|
+
|
|
294
|
+
# 調査の結果、'style' プロパティへの代入が正しい設定方法と判明
|
|
295
|
+
self.plotter.interactor.SetInteractorStyle(style)
|
|
296
|
+
self.plotter.interactor.Initialize()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def dragEnterEvent(self, event):
|
|
301
|
+
"""ウィンドウ全体でサポートされているファイルのドラッグを受け入れる"""
|
|
302
|
+
# Accept if any dragged local file has a supported extension
|
|
303
|
+
if event.mimeData().hasUrls():
|
|
304
|
+
urls = event.mimeData().urls()
|
|
305
|
+
for url in urls:
|
|
306
|
+
try:
|
|
307
|
+
if url.isLocalFile():
|
|
308
|
+
file_path = url.toLocalFile()
|
|
309
|
+
file_lower = file_path.lower()
|
|
310
|
+
|
|
311
|
+
# Built-in extensions
|
|
312
|
+
if file_lower.endswith(('.pmeraw', '.pmeprj', '.mol', '.sdf', '.xyz')):
|
|
313
|
+
event.acceptProposedAction()
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# 2. Plugin drop handlers (Drop専用ハンドラ)
|
|
317
|
+
# プラグインが「Dropを受け入れる」と明示している場合のみ許可
|
|
318
|
+
|
|
319
|
+
# Plugin drop handlers (accept more liberally for custom logic)
|
|
320
|
+
# A plugin drop handler might handle it, so accept
|
|
321
|
+
if self.plugin_manager and hasattr(self.plugin_manager, 'drop_handlers'):
|
|
322
|
+
if len(self.plugin_manager.drop_handlers) > 0:
|
|
323
|
+
# Accept any file if drop handlers are registered
|
|
324
|
+
# They will check the file type in dropEvent
|
|
325
|
+
event.acceptProposedAction()
|
|
326
|
+
return
|
|
327
|
+
except Exception:
|
|
328
|
+
continue
|
|
329
|
+
event.ignore()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def dropEvent(self, event):
|
|
334
|
+
"""ファイルがウィンドウ上でドロップされたときに呼び出される"""
|
|
335
|
+
urls = event.mimeData().urls()
|
|
336
|
+
# Find the first local file from the dropped URLs
|
|
337
|
+
file_path = None
|
|
338
|
+
if urls:
|
|
339
|
+
for url in urls:
|
|
340
|
+
try:
|
|
341
|
+
if url.isLocalFile():
|
|
342
|
+
file_path = url.toLocalFile()
|
|
343
|
+
break
|
|
344
|
+
except Exception:
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
if file_path:
|
|
348
|
+
# 1. Custom Plugin Handlers
|
|
349
|
+
if self.plugin_manager and hasattr(self.plugin_manager, 'drop_handlers'):
|
|
350
|
+
for handler_def in self.plugin_manager.drop_handlers:
|
|
351
|
+
try:
|
|
352
|
+
callback = handler_def['callback']
|
|
353
|
+
handled = callback(file_path)
|
|
354
|
+
if handled:
|
|
355
|
+
event.acceptProposedAction()
|
|
356
|
+
return
|
|
357
|
+
except Exception as e:
|
|
358
|
+
print(f"Error in plugin drop handler: {e}")
|
|
359
|
+
# ドロップ位置を取得
|
|
360
|
+
drop_pos = event.position().toPoint()
|
|
361
|
+
# 拡張子に応じて適切な読み込みメソッドを呼び出す
|
|
362
|
+
if file_path.lower().endswith((".pmeraw", ".pmeprj")):
|
|
363
|
+
self.open_project_file(file_path=file_path)
|
|
364
|
+
QTimer.singleShot(100, self.fit_to_view) # 遅延でFit
|
|
365
|
+
event.acceptProposedAction()
|
|
366
|
+
elif file_path.lower().endswith((".mol", ".sdf")):
|
|
367
|
+
plotter_widget = self.splitter.widget(1) # 3Dビューアーウィジェット
|
|
368
|
+
plotter_rect = plotter_widget.geometry()
|
|
369
|
+
if plotter_rect.contains(drop_pos):
|
|
370
|
+
self.load_mol_file_for_3d_viewing(file_path=file_path)
|
|
371
|
+
else:
|
|
372
|
+
if hasattr(self, "load_mol_file"):
|
|
373
|
+
self.load_mol_file(file_path=file_path)
|
|
374
|
+
else:
|
|
375
|
+
self.statusBar().showMessage("MOL file import not implemented for 2D editor.")
|
|
376
|
+
QTimer.singleShot(100, self.fit_to_view) # 遅延でFit
|
|
377
|
+
event.acceptProposedAction()
|
|
378
|
+
elif file_path.lower().endswith(".xyz"):
|
|
379
|
+
self.load_xyz_for_3d_viewing(file_path=file_path)
|
|
380
|
+
QTimer.singleShot(100, self.fit_to_view) # 遅延でFit
|
|
381
|
+
event.acceptProposedAction()
|
|
382
|
+
else:
|
|
383
|
+
self.statusBar().showMessage(f"Unsupported file type: {file_path}")
|
|
384
|
+
event.ignore()
|
|
385
|
+
else:
|
|
386
|
+
event.ignore()
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _enable_3d_edit_actions(self, enabled=True):
|
|
391
|
+
"""3D編集機能のアクションを統一的に有効/無効化する"""
|
|
392
|
+
actions = [
|
|
393
|
+
'translation_action',
|
|
394
|
+
'move_group_action',
|
|
395
|
+
'alignplane_xy_action',
|
|
396
|
+
'alignplane_xz_action',
|
|
397
|
+
'alignplane_yz_action',
|
|
398
|
+
'align_x_action',
|
|
399
|
+
'align_y_action',
|
|
400
|
+
'align_z_action',
|
|
401
|
+
'bond_length_action',
|
|
402
|
+
'angle_action',
|
|
403
|
+
'dihedral_action',
|
|
404
|
+
'mirror_action',
|
|
405
|
+
'planarize_action',
|
|
406
|
+
'constrained_opt_action'
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
# メニューとサブメニューも有効/無効化
|
|
410
|
+
menus = [
|
|
411
|
+
'align_menu'
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
for action_name in actions:
|
|
415
|
+
if hasattr(self, action_name):
|
|
416
|
+
getattr(self, action_name).setEnabled(enabled)
|
|
417
|
+
|
|
418
|
+
for menu_name in menus:
|
|
419
|
+
if hasattr(self, menu_name):
|
|
420
|
+
getattr(self, menu_name).setEnabled(enabled)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _enable_3d_features(self, enabled=True):
|
|
425
|
+
"""3D関連機能を統一的に有効/無効化する"""
|
|
426
|
+
# 基本的な3D機能(3D SelectとEditは除外して常に有効にする)
|
|
427
|
+
basic_3d_actions = [
|
|
428
|
+
'optimize_3d_button',
|
|
429
|
+
'export_button',
|
|
430
|
+
'analysis_action'
|
|
431
|
+
]
|
|
432
|
+
|
|
433
|
+
for action_name in basic_3d_actions:
|
|
434
|
+
if hasattr(self, action_name):
|
|
435
|
+
# If enabling globally but chemical sanitization failed earlier, keep Optimize 3D disabled
|
|
436
|
+
# Keep Optimize disabled when any of these conditions are true:
|
|
437
|
+
# - we're globally disabling 3D features (enabled==False)
|
|
438
|
+
# - the current molecule was created via the "skip chemistry checks" XYZ path
|
|
439
|
+
# - a prior chemistry check was attempted and failed
|
|
440
|
+
if action_name == 'optimize_3d_button':
|
|
441
|
+
try:
|
|
442
|
+
# If we're disabling all 3D features, ensure Optimize is disabled
|
|
443
|
+
if not enabled:
|
|
444
|
+
getattr(self, action_name).setEnabled(False)
|
|
445
|
+
continue
|
|
446
|
+
|
|
447
|
+
# If the current molecule was marked as XYZ-derived (skip path), keep Optimize disabled
|
|
448
|
+
if getattr(self, 'is_xyz_derived', False):
|
|
449
|
+
getattr(self, action_name).setEnabled(False)
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
# If a chemistry check was tried and failed, keep Optimize disabled
|
|
453
|
+
if getattr(self, 'chem_check_tried', False) and getattr(self, 'chem_check_failed', False):
|
|
454
|
+
getattr(self, action_name).setEnabled(False)
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
# Otherwise enable/disable according to the requested global flag
|
|
458
|
+
getattr(self, action_name).setEnabled(bool(enabled))
|
|
459
|
+
except Exception:
|
|
460
|
+
pass
|
|
461
|
+
else:
|
|
462
|
+
try:
|
|
463
|
+
getattr(self, action_name).setEnabled(enabled)
|
|
464
|
+
except Exception:
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
# 3D Selectボタンは常に有効にする
|
|
468
|
+
if hasattr(self, 'measurement_action'):
|
|
469
|
+
self.measurement_action.setEnabled(True)
|
|
470
|
+
|
|
471
|
+
# 3D Dragボタンも常に有効にする
|
|
472
|
+
if hasattr(self, 'edit_3d_action'):
|
|
473
|
+
self.edit_3d_action.setEnabled(True)
|
|
474
|
+
|
|
475
|
+
# 3D編集機能も含める
|
|
476
|
+
if enabled:
|
|
477
|
+
self._enable_3d_edit_actions(True)
|
|
478
|
+
else:
|
|
479
|
+
self._enable_3d_edit_actions(False)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _enter_3d_viewer_ui_mode(self):
|
|
484
|
+
"""3DビューアモードのUI状態に設定する"""
|
|
485
|
+
self.is_2d_editable = False
|
|
486
|
+
self.cleanup_button.setEnabled(False)
|
|
487
|
+
self.convert_button.setEnabled(False)
|
|
488
|
+
for action in self.tool_group.actions():
|
|
489
|
+
action.setEnabled(False)
|
|
490
|
+
if hasattr(self, 'other_atom_action'):
|
|
491
|
+
self.other_atom_action.setEnabled(False)
|
|
492
|
+
|
|
493
|
+
self.minimize_2d_panel()
|
|
494
|
+
|
|
495
|
+
# 3D関連機能を統一的に有効化
|
|
496
|
+
self._enable_3d_features(True)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def restore_ui_for_editing(self):
|
|
501
|
+
"""Enables all 2D editing UI elements."""
|
|
502
|
+
self.is_2d_editable = True
|
|
503
|
+
self.restore_2d_panel()
|
|
504
|
+
self.cleanup_button.setEnabled(True)
|
|
505
|
+
self.convert_button.setEnabled(True)
|
|
506
|
+
|
|
507
|
+
for action in self.tool_group.actions():
|
|
508
|
+
action.setEnabled(True)
|
|
509
|
+
|
|
510
|
+
if hasattr(self, 'other_atom_action'):
|
|
511
|
+
self.other_atom_action.setEnabled(True)
|
|
512
|
+
|
|
513
|
+
# 2Dモードに戻る時は3D編集機能を統一的に無効化
|
|
514
|
+
self._enable_3d_edit_actions(False)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def minimize_2d_panel(self):
|
|
519
|
+
"""2Dパネルを最小化(非表示に)する"""
|
|
520
|
+
sizes = self.splitter.sizes()
|
|
521
|
+
# すでに最小化されていなければ実行
|
|
522
|
+
if sizes[0] > 0:
|
|
523
|
+
total_width = sum(sizes)
|
|
524
|
+
self.splitter.setSizes([0, total_width])
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def restore_2d_panel(self):
|
|
529
|
+
"""最小化された2Dパネルを元のサイズに戻す"""
|
|
530
|
+
sizes = self.splitter.sizes()
|
|
531
|
+
|
|
532
|
+
# sizesリストが空でないことを確認してからアクセスする
|
|
533
|
+
if sizes and sizes[0] == 0:
|
|
534
|
+
self.splitter.setSizes([600, 600])
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def set_panel_layout(self, left_percent, right_percent):
|
|
539
|
+
"""パネルレイアウトを指定した比率に設定する"""
|
|
540
|
+
if left_percent + right_percent != 100:
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
total_width = self.splitter.width()
|
|
544
|
+
if total_width <= 0:
|
|
545
|
+
total_width = 1200 # デフォルト幅
|
|
546
|
+
|
|
547
|
+
left_width = int(total_width * left_percent / 100)
|
|
548
|
+
right_width = int(total_width * right_percent / 100)
|
|
549
|
+
|
|
550
|
+
self.splitter.setSizes([left_width, right_width])
|
|
551
|
+
|
|
552
|
+
# ユーザーにフィードバック表示
|
|
553
|
+
self.statusBar().showMessage(
|
|
554
|
+
f"Panel layout set to {left_percent}% : {right_percent}%",
|
|
555
|
+
2000
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def toggle_2d_panel(self):
|
|
561
|
+
"""2Dパネルの表示/非表示を切り替える"""
|
|
562
|
+
sizes = self.splitter.sizes()
|
|
563
|
+
if not sizes:
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
if sizes[0] == 0:
|
|
567
|
+
# 2Dパネルが非表示の場合は表示
|
|
568
|
+
self.restore_2d_panel()
|
|
569
|
+
self.statusBar().showMessage("2D panel restored", 1500)
|
|
570
|
+
else:
|
|
571
|
+
# 2Dパネルが表示されている場合は非表示
|
|
572
|
+
self.minimize_2d_panel()
|
|
573
|
+
self.statusBar().showMessage("2D panel minimized", 1500)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def on_splitter_moved(self, pos, index):
|
|
578
|
+
"""スプリッターが移動された時のフィードバック表示"""
|
|
579
|
+
sizes = self.splitter.sizes()
|
|
580
|
+
if len(sizes) >= 2:
|
|
581
|
+
total = sum(sizes)
|
|
582
|
+
if total > 0:
|
|
583
|
+
left_percent = round(sizes[0] * 100 / total)
|
|
584
|
+
right_percent = round(sizes[1] * 100 / total)
|
|
585
|
+
|
|
586
|
+
# 現在の比率をツールチップで表示
|
|
587
|
+
if hasattr(self.splitter, 'handle'):
|
|
588
|
+
handle = self.splitter.handle(1)
|
|
589
|
+
if handle:
|
|
590
|
+
handle.setToolTip(f"2D: {left_percent}% | 3D: {right_percent}%")
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def setup_splitter_tooltip(self):
|
|
595
|
+
"""スプリッターハンドルの初期ツールチップを設定"""
|
|
596
|
+
handle = self.splitter.handle(1)
|
|
597
|
+
if handle:
|
|
598
|
+
handle.setToolTip("Drag to resize panels | Ctrl+1/2/3 for presets | Ctrl+H to toggle 2D panel")
|
|
599
|
+
# 初期サイズ比率も表示
|
|
600
|
+
self.on_splitter_moved(0, 0)
|
|
601
|
+
|
|
602
|
+
|