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