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,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
+