MoleditPy 2.1.1__py3-none-any.whl → 2.2.0a0__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/modules/constants.py +1 -1
- moleditpy/modules/main_window.py +8 -0
- moleditpy/modules/main_window_app_state.py +24 -0
- moleditpy/modules/main_window_compute.py +27 -0
- moleditpy/modules/main_window_main_init.py +274 -56
- moleditpy/modules/main_window_ui_manager.py +12 -1
- moleditpy/modules/main_window_view_3d.py +122 -35
- moleditpy/modules/molecule_scene.py +6 -14
- moleditpy/modules/plugin_interface.py +204 -0
- moleditpy/modules/plugin_manager.py +220 -38
- moleditpy/modules/plugin_manager_window.py +218 -0
- moleditpy/modules/template_preview_item.py +3 -6
- moleditpy/modules/user_template_dialog.py +59 -1
- {moleditpy-2.1.1.dist-info → moleditpy-2.2.0a0.dist-info}/METADATA +1 -1
- {moleditpy-2.1.1.dist-info → moleditpy-2.2.0a0.dist-info}/RECORD +19 -17
- {moleditpy-2.1.1.dist-info → moleditpy-2.2.0a0.dist-info}/WHEEL +0 -0
- {moleditpy-2.1.1.dist-info → moleditpy-2.2.0a0.dist-info}/entry_points.txt +0 -0
- {moleditpy-2.1.1.dist-info → moleditpy-2.2.0a0.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-2.1.1.dist-info → moleditpy-2.2.0a0.dist-info}/top_level.txt +0 -0
moleditpy/modules/constants.py
CHANGED
moleditpy/modules/main_window.py
CHANGED
|
@@ -600,6 +600,14 @@ class MainWindow(QMainWindow):
|
|
|
600
600
|
# --- MOVED TO main_window_view_3d.py ---
|
|
601
601
|
return self.main_window_view_3d.fit_to_view()
|
|
602
602
|
|
|
603
|
+
def draw_standard_3d_style(self, mol, style_override=None):
|
|
604
|
+
# --- MOVED TO main_window_view_3d.py ---
|
|
605
|
+
return self.main_window_view_3d.draw_standard_3d_style(mol, style_override)
|
|
606
|
+
|
|
607
|
+
def clear_measurement_selection(self):
|
|
608
|
+
# --- MOVED TO main_window_view_3d.py ---
|
|
609
|
+
return self.main_window_view_3d.clear_measurement_selection()
|
|
610
|
+
|
|
603
611
|
def toggle_3d_edit_mode(self, checked):
|
|
604
612
|
# --- MOVED TO main_window_ui_manager.py ---
|
|
605
613
|
return self.main_window_ui_manager.toggle_3d_edit_mode(checked)
|
|
@@ -595,6 +595,20 @@ class MainWindowAppState(object):
|
|
|
595
595
|
except Exception:
|
|
596
596
|
json_data["last_successful_optimization_method"] = None
|
|
597
597
|
|
|
598
|
+
# Plugin State Persistence (Phase 3)
|
|
599
|
+
if self.plugin_manager and self.plugin_manager.save_handlers:
|
|
600
|
+
plugin_data = {}
|
|
601
|
+
for name, callback in self.plugin_manager.save_handlers.items():
|
|
602
|
+
try:
|
|
603
|
+
p_state = callback()
|
|
604
|
+
# Ensure serializable? Use primitive types ideally.
|
|
605
|
+
plugin_data[name] = p_state
|
|
606
|
+
except Exception as e:
|
|
607
|
+
print(f"Error saving state for plugin {name}: {e}")
|
|
608
|
+
|
|
609
|
+
if plugin_data:
|
|
610
|
+
json_data['plugins'] = plugin_data
|
|
611
|
+
|
|
598
612
|
return json_data
|
|
599
613
|
|
|
600
614
|
|
|
@@ -614,6 +628,16 @@ class MainWindowAppState(object):
|
|
|
614
628
|
except Exception:
|
|
615
629
|
self.last_successful_optimization_method = None
|
|
616
630
|
|
|
631
|
+
# Plugin State Restoration (Phase 3)
|
|
632
|
+
if "plugins" in json_data and self.plugin_manager and self.plugin_manager.load_handlers:
|
|
633
|
+
plugin_data = json_data["plugins"]
|
|
634
|
+
for name, p_state in plugin_data.items():
|
|
635
|
+
if name in self.plugin_manager.load_handlers:
|
|
636
|
+
try:
|
|
637
|
+
self.plugin_manager.load_handlers[name](p_state)
|
|
638
|
+
except Exception as e:
|
|
639
|
+
print(f"Error loading state for plugin {name}: {e}")
|
|
640
|
+
|
|
617
641
|
|
|
618
642
|
# 2D構造データの復元
|
|
619
643
|
if "2d_structure" in json_data:
|
|
@@ -145,6 +145,11 @@ class MainWindowCompute(object):
|
|
|
145
145
|
"""右クリックで表示する一時的な3D変換メニュー。
|
|
146
146
|
選択したモードは一時フラグとして保持され、その後の変換で使用されます(永続化しません)。
|
|
147
147
|
"""
|
|
148
|
+
# If button is disabled (during calculation), do not show menu
|
|
149
|
+
if not self.convert_button.isEnabled():
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
|
|
148
153
|
try:
|
|
149
154
|
menu = QMenu(self)
|
|
150
155
|
conv_options = [
|
|
@@ -203,6 +208,16 @@ class MainWindowCompute(object):
|
|
|
203
208
|
a.triggered.connect(lambda checked=False, k=key: self._trigger_optimize_with_temp_method(k))
|
|
204
209
|
menu.addAction(a)
|
|
205
210
|
|
|
211
|
+
# Add Plugin Optimization Methods
|
|
212
|
+
if hasattr(self.mw, 'plugin_manager') and self.mw.plugin_manager.optimization_methods:
|
|
213
|
+
methods = self.mw.plugin_manager.optimization_methods
|
|
214
|
+
if methods:
|
|
215
|
+
menu.addSeparator()
|
|
216
|
+
for method_name, info in methods.items():
|
|
217
|
+
a = QAction(info.get('label', method_name), self)
|
|
218
|
+
a.triggered.connect(lambda checked=False, k=method_name: self._trigger_optimize_with_temp_method(k))
|
|
219
|
+
menu.addAction(a)
|
|
220
|
+
|
|
206
221
|
menu.exec_(self.optimize_3d_button.mapToGlobal(pos))
|
|
207
222
|
except Exception as e:
|
|
208
223
|
print(f"Error showing optimize menu: {e}")
|
|
@@ -765,6 +780,18 @@ class MainWindowCompute(object):
|
|
|
765
780
|
except Exception as e:
|
|
766
781
|
self.statusBar().showMessage(f"UFF (RDKit) optimization error: {e}")
|
|
767
782
|
return
|
|
783
|
+
# Plugin method dispatch
|
|
784
|
+
elif hasattr(self.mw, 'plugin_manager') and hasattr(self.mw.plugin_manager, 'optimization_methods') and method in self.mw.plugin_manager.optimization_methods:
|
|
785
|
+
info = self.mw.plugin_manager.optimization_methods[method]
|
|
786
|
+
callback = info['callback']
|
|
787
|
+
try:
|
|
788
|
+
success = callback(self.current_mol)
|
|
789
|
+
if not success:
|
|
790
|
+
self.statusBar().showMessage(f"Optimization method '{method}' returned failure.")
|
|
791
|
+
return
|
|
792
|
+
except Exception as e:
|
|
793
|
+
self.statusBar().showMessage(f"Plugin optimization error ({method}): {e}")
|
|
794
|
+
return
|
|
768
795
|
else:
|
|
769
796
|
self.statusBar().showMessage("Selected optimization method is not available. Use MMFF94 (RDKit) or UFF (RDKit).")
|
|
770
797
|
return
|
|
@@ -328,7 +328,7 @@ class MainWindowMainInit(object):
|
|
|
328
328
|
else:
|
|
329
329
|
print(f"警告: アイコンファイルが見つかりません: {icon_path}")
|
|
330
330
|
|
|
331
|
-
|
|
331
|
+
|
|
332
332
|
|
|
333
333
|
self.splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
334
334
|
# スプリッターハンドルを太くして視認性を向上
|
|
@@ -395,10 +395,11 @@ class MainWindowMainInit(object):
|
|
|
395
395
|
self.plotter.setSizePolicy(
|
|
396
396
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
397
397
|
)
|
|
398
|
+
self.plotter.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
|
|
399
|
+
|
|
398
400
|
# 2. 垂直レイアウトに3Dビューを追加
|
|
399
401
|
right_layout.addWidget(self.plotter, 1)
|
|
400
402
|
#self.plotter.installEventFilter(self)
|
|
401
|
-
|
|
402
403
|
# 3. ボタンをまとめるための「水平」レイアウトを作成
|
|
403
404
|
right_buttons_layout = QHBoxLayout()
|
|
404
405
|
|
|
@@ -414,6 +415,7 @@ class MainWindowMainInit(object):
|
|
|
414
415
|
self.optimize_3d_button.customContextMenuRequested.connect(self.show_optimize_menu)
|
|
415
416
|
except Exception:
|
|
416
417
|
pass
|
|
418
|
+
pass
|
|
417
419
|
right_buttons_layout.addWidget(self.optimize_3d_button)
|
|
418
420
|
|
|
419
421
|
# エクスポートボタン (メニュー付き)
|
|
@@ -467,6 +469,9 @@ class MainWindowMainInit(object):
|
|
|
467
469
|
# Keep a reference to the main toolbar for later updates
|
|
468
470
|
self.toolbar = toolbar
|
|
469
471
|
|
|
472
|
+
# Now that toolbar exists, initialize menu bar (which might add toolbar actions from plugins)
|
|
473
|
+
# self.init_menu_bar() - Moved down
|
|
474
|
+
|
|
470
475
|
# Templates toolbar: place it directly below the main toolbar (second row at the top)
|
|
471
476
|
# Use addToolBarBreak to ensure this toolbar appears on the next row under the main toolbar.
|
|
472
477
|
# Some older PyQt/PySide versions may not have addToolBarBreak; fall back silently in that case.
|
|
@@ -480,6 +485,20 @@ class MainWindowMainInit(object):
|
|
|
480
485
|
toolbar_bottom = QToolBar("Templates Toolbar")
|
|
481
486
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar_bottom)
|
|
482
487
|
self.toolbar_bottom = toolbar_bottom
|
|
488
|
+
|
|
489
|
+
# Plugin Toolbar (Third Row)
|
|
490
|
+
try:
|
|
491
|
+
self.addToolBarBreak(Qt.ToolBarArea.TopToolBarArea)
|
|
492
|
+
except Exception:
|
|
493
|
+
pass
|
|
494
|
+
|
|
495
|
+
self.plugin_toolbar = QToolBar("Plugin Toolbar")
|
|
496
|
+
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.plugin_toolbar)
|
|
497
|
+
self.plugin_toolbar.hide()
|
|
498
|
+
|
|
499
|
+
# Initialize menu bar (and populate toolbars) AFTER all toolbars are created
|
|
500
|
+
self.init_menu_bar()
|
|
501
|
+
|
|
483
502
|
self.tool_group = QActionGroup(self)
|
|
484
503
|
self.tool_group.setExclusive(True)
|
|
485
504
|
|
|
@@ -853,30 +872,31 @@ class MainWindowMainInit(object):
|
|
|
853
872
|
|
|
854
873
|
file_menu.addSeparator()
|
|
855
874
|
|
|
875
|
+
|
|
856
876
|
# === インポート ===
|
|
857
|
-
import_menu = file_menu.addMenu("Import")
|
|
877
|
+
self.import_menu = file_menu.addMenu("Import")
|
|
858
878
|
|
|
859
879
|
load_mol_action = QAction("MOL/SDF File...", self)
|
|
860
880
|
load_mol_action.triggered.connect(self.load_mol_file)
|
|
861
|
-
import_menu.addAction(load_mol_action)
|
|
881
|
+
self.import_menu.addAction(load_mol_action)
|
|
862
882
|
|
|
863
883
|
import_smiles_action = QAction("SMILES...", self)
|
|
864
884
|
import_smiles_action.triggered.connect(self.import_smiles_dialog)
|
|
865
|
-
import_menu.addAction(import_smiles_action)
|
|
885
|
+
self.import_menu.addAction(import_smiles_action)
|
|
866
886
|
|
|
867
887
|
import_inchi_action = QAction("InChI...", self)
|
|
868
888
|
import_inchi_action.triggered.connect(self.import_inchi_dialog)
|
|
869
|
-
import_menu.addAction(import_inchi_action)
|
|
889
|
+
self.import_menu.addAction(import_inchi_action)
|
|
870
890
|
|
|
871
|
-
import_menu.addSeparator()
|
|
891
|
+
self.import_menu.addSeparator()
|
|
872
892
|
|
|
873
893
|
load_3d_mol_action = QAction("3D MOL/SDF (3D View Only)...", self)
|
|
874
894
|
load_3d_mol_action.triggered.connect(self.load_mol_file_for_3d_viewing)
|
|
875
|
-
import_menu.addAction(load_3d_mol_action)
|
|
895
|
+
self.import_menu.addAction(load_3d_mol_action)
|
|
876
896
|
|
|
877
897
|
load_3d_xyz_action = QAction("3D XYZ (3D View Only)...", self)
|
|
878
898
|
load_3d_xyz_action.triggered.connect(self.load_xyz_for_3d_viewing)
|
|
879
|
-
import_menu.addAction(load_3d_xyz_action)
|
|
899
|
+
self.import_menu.addAction(load_3d_xyz_action)
|
|
880
900
|
|
|
881
901
|
# === エクスポート ===
|
|
882
902
|
export_menu = file_menu.addMenu("Export")
|
|
@@ -1204,16 +1224,16 @@ class MainWindowMainInit(object):
|
|
|
1204
1224
|
# Plugin menu
|
|
1205
1225
|
plugin_menu = menu_bar.addMenu("&Plugin")
|
|
1206
1226
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1227
|
+
# Only keep the Manager action, moving others to the Manager Window
|
|
1228
|
+
manage_plugins_action = QAction("Plugin Manager...", self)
|
|
1229
|
+
def show_plugin_manager():
|
|
1230
|
+
from .plugin_manager_window import PluginManagerWindow
|
|
1231
|
+
dlg = PluginManagerWindow(self.plugin_manager, self)
|
|
1232
|
+
dlg.exec()
|
|
1233
|
+
self.update_plugin_menu(plugin_menu) # Refresh after closing
|
|
1234
|
+
manage_plugins_action.triggered.connect(show_plugin_manager)
|
|
1235
|
+
plugin_menu.addAction(manage_plugins_action)
|
|
1236
|
+
|
|
1217
1237
|
|
|
1218
1238
|
|
|
1219
1239
|
|
|
@@ -1386,6 +1406,8 @@ class MainWindowMainInit(object):
|
|
|
1386
1406
|
)
|
|
1387
1407
|
help_menu.addAction(manual_action)
|
|
1388
1408
|
|
|
1409
|
+
|
|
1410
|
+
|
|
1389
1411
|
# 3D関連機能の初期状態を統一的に設定
|
|
1390
1412
|
self._enable_3d_features(False)
|
|
1391
1413
|
|
|
@@ -1418,7 +1440,27 @@ class MainWindowMainInit(object):
|
|
|
1418
1440
|
if not file_path or not os.path.exists(file_path):
|
|
1419
1441
|
return
|
|
1420
1442
|
|
|
1421
|
-
|
|
1443
|
+
# Helper for extension
|
|
1444
|
+
_, ext_with_dot = os.path.splitext(file_path)
|
|
1445
|
+
ext_with_dot = ext_with_dot.lower()
|
|
1446
|
+
# Legacy variable name (no dot)
|
|
1447
|
+
file_ext = ext_with_dot.lstrip('.')
|
|
1448
|
+
|
|
1449
|
+
# 1. Custom Plugin Openers
|
|
1450
|
+
# 1. Custom Plugin Openers
|
|
1451
|
+
if ext_with_dot in self.plugin_manager.file_openers:
|
|
1452
|
+
opener = self.plugin_manager.file_openers[ext_with_dot]
|
|
1453
|
+
try:
|
|
1454
|
+
opener['callback'](file_path)
|
|
1455
|
+
self.current_file_path = file_path
|
|
1456
|
+
self.update_window_title()
|
|
1457
|
+
return
|
|
1458
|
+
except Exception as e:
|
|
1459
|
+
print(f"Plugin opener failed: {e}")
|
|
1460
|
+
QMessageBox.warning(self, "Plugin Error", f"Error opening file with plugin '{opener.get('plugin', 'Unknown')}':\n{e}")
|
|
1461
|
+
# Fallback to standard logic if plugin fails? Or stop?
|
|
1462
|
+
# Generally if a plugin claims it, we stop. But here we let it fall through if it errors?
|
|
1463
|
+
# Let's simple check next.
|
|
1422
1464
|
|
|
1423
1465
|
if file_ext in ['mol', 'sdf']:
|
|
1424
1466
|
self.load_mol_file_for_3d_viewing(file_path)
|
|
@@ -1717,26 +1759,88 @@ class MainWindowMainInit(object):
|
|
|
1717
1759
|
# Clear existing plugin actions
|
|
1718
1760
|
plugin_menu.clear()
|
|
1719
1761
|
|
|
1720
|
-
#
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
explore_plugins_action = QAction("Explore Plugins", self)
|
|
1730
|
-
explore_plugins_action.triggered.connect(
|
|
1731
|
-
lambda: QDesktopServices.openUrl(QUrl("https://hiroyokoyama.github.io/moleditpy-plugins/explorer/"))
|
|
1732
|
-
)
|
|
1733
|
-
plugin_menu.addAction(explore_plugins_action)
|
|
1762
|
+
# Only keep the Manager action
|
|
1763
|
+
manage_plugins_action = QAction("Plugin Manager...", self)
|
|
1764
|
+
def show_plugin_manager():
|
|
1765
|
+
from .plugin_manager_window import PluginManagerWindow
|
|
1766
|
+
dlg = PluginManagerWindow(self.plugin_manager, self)
|
|
1767
|
+
dlg.exec()
|
|
1768
|
+
self.update_plugin_menu(plugin_menu) # Refresh after closing
|
|
1769
|
+
manage_plugins_action.triggered.connect(show_plugin_manager)
|
|
1770
|
+
plugin_menu.addAction(manage_plugins_action)
|
|
1734
1771
|
|
|
1735
1772
|
plugin_menu.addSeparator()
|
|
1736
1773
|
|
|
1737
|
-
# Add dynamic plugin actions
|
|
1774
|
+
# Add dynamic plugin actions (Legacy + New Registration)
|
|
1738
1775
|
plugins = self.plugin_manager.discover_plugins(self)
|
|
1739
1776
|
|
|
1777
|
+
# 1. Add Registered Menu Actions (New System)
|
|
1778
|
+
if self.plugin_manager.menu_actions:
|
|
1779
|
+
for action_def in self.plugin_manager.menu_actions:
|
|
1780
|
+
path = action_def['path']
|
|
1781
|
+
callback = action_def['callback']
|
|
1782
|
+
text = action_def['text']
|
|
1783
|
+
# Create/Find menu path
|
|
1784
|
+
current_menu = self.menuBar() # Or find specific top-level
|
|
1785
|
+
|
|
1786
|
+
# Handling top-level menus vs nested
|
|
1787
|
+
parts = path.split('/')
|
|
1788
|
+
|
|
1789
|
+
# If path starts with existing top-level (File, Edit, etc), grab it
|
|
1790
|
+
# Otherwise create new top-level
|
|
1791
|
+
top_level_title = parts[0]
|
|
1792
|
+
found_top = False
|
|
1793
|
+
for act in self.menuBar().actions():
|
|
1794
|
+
if act.menu() and act.text().replace('&', '') == top_level_title:
|
|
1795
|
+
current_menu = act.menu()
|
|
1796
|
+
found_top = True
|
|
1797
|
+
break
|
|
1798
|
+
|
|
1799
|
+
if not found_top:
|
|
1800
|
+
current_menu = self.menuBar().addMenu(top_level_title)
|
|
1801
|
+
|
|
1802
|
+
# Traverse rest
|
|
1803
|
+
for part in parts[1:]:
|
|
1804
|
+
found_sub = False
|
|
1805
|
+
for act in current_menu.actions():
|
|
1806
|
+
if act.menu() and act.text().replace('&', '') == part:
|
|
1807
|
+
current_menu = act.menu()
|
|
1808
|
+
found_sub = True
|
|
1809
|
+
break
|
|
1810
|
+
if not found_sub:
|
|
1811
|
+
current_menu = current_menu.addMenu(part)
|
|
1812
|
+
|
|
1813
|
+
# Add action
|
|
1814
|
+
action_text = text if text else parts[-1]
|
|
1815
|
+
action = QAction(action_text, self)
|
|
1816
|
+
action.triggered.connect(callback)
|
|
1817
|
+
current_menu.addAction(action)
|
|
1818
|
+
|
|
1819
|
+
# 2. Add Toolbar Buttons (New System)
|
|
1820
|
+
# 2. Add Toolbar Buttons (New System)
|
|
1821
|
+
# Use dedicated plugin toolbar
|
|
1822
|
+
if hasattr(self, 'plugin_toolbar'):
|
|
1823
|
+
self.plugin_toolbar.clear()
|
|
1824
|
+
|
|
1825
|
+
if self.plugin_manager.toolbar_actions:
|
|
1826
|
+
self.plugin_toolbar.show()
|
|
1827
|
+
for action_def in self.plugin_manager.toolbar_actions:
|
|
1828
|
+
text = action_def['text']
|
|
1829
|
+
callback = action_def['callback']
|
|
1830
|
+
|
|
1831
|
+
action = QAction(text, self)
|
|
1832
|
+
action.triggered.connect(callback)
|
|
1833
|
+
if action_def['icon']:
|
|
1834
|
+
if os.path.exists(action_def['icon']):
|
|
1835
|
+
action.setIcon(QIcon(action_def['icon']))
|
|
1836
|
+
if action_def['tooltip']:
|
|
1837
|
+
action.setToolTip(action_def['tooltip'])
|
|
1838
|
+
self.plugin_toolbar.addAction(action)
|
|
1839
|
+
else:
|
|
1840
|
+
self.plugin_toolbar.hide()
|
|
1841
|
+
|
|
1842
|
+
# 3. Legacy Menu Building (Folder based)
|
|
1843
|
+
|
|
1740
1844
|
if not plugins:
|
|
1741
1845
|
no_plugin_action = QAction("(No plugins found)", self)
|
|
1742
1846
|
no_plugin_action.setEnabled(False)
|
|
@@ -1750,28 +1854,142 @@ class MainWindowMainInit(object):
|
|
|
1750
1854
|
menus = { "": plugin_menu }
|
|
1751
1855
|
|
|
1752
1856
|
for p in plugins:
|
|
1753
|
-
|
|
1857
|
+
# Only add legacy plugins (with 'run' function) to the generic Plugins menu.
|
|
1858
|
+
# New plugins (with 'initialize') should register their own menu actions if needed.
|
|
1859
|
+
if hasattr(p['module'], 'run'):
|
|
1860
|
+
rel_folder = p.get('rel_folder', '')
|
|
1861
|
+
# Get or create the parent menu for this plugin
|
|
1862
|
+
parent_menu = menus.get("") # Start at root
|
|
1863
|
+
|
|
1864
|
+
if rel_folder:
|
|
1865
|
+
# Split path and traverse/create submenus
|
|
1866
|
+
parts = rel_folder.split(os.sep)
|
|
1867
|
+
current_path = ""
|
|
1868
|
+
for part in parts:
|
|
1869
|
+
new_path = os.path.join(current_path, part) if current_path else part
|
|
1870
|
+
|
|
1871
|
+
if new_path not in menus:
|
|
1872
|
+
# Create new submenu
|
|
1873
|
+
sub_menu = parent_menu.addMenu(part)
|
|
1874
|
+
menus[new_path] = sub_menu
|
|
1875
|
+
|
|
1876
|
+
parent_menu = menus[new_path]
|
|
1877
|
+
current_path = new_path
|
|
1878
|
+
|
|
1879
|
+
# Add action to the resolved parent_menu
|
|
1880
|
+
action = QAction(p['name'], self)
|
|
1881
|
+
action.triggered.connect(lambda checked, mod=p['module']: self.plugin_manager.run_plugin(mod, self.mw if hasattr(self, 'mw') else self))
|
|
1882
|
+
parent_menu.addAction(action)
|
|
1883
|
+
|
|
1884
|
+
# 4. Integrate Export Actions into Export Button and Menu
|
|
1885
|
+
if self.plugin_manager.export_actions:
|
|
1886
|
+
# Add separator if we have custom exports (and haven't added it yet for this session/update)
|
|
1887
|
+
# Since update_plugin_menu clears the dynamic plugin actions but NOT the export menu (which is on a button),
|
|
1888
|
+
# we need to be careful not to duplicate.
|
|
1889
|
+
# Ideally, we should clear custom actions from the export menu first.
|
|
1890
|
+
# For simplicity, we'll just check existence.
|
|
1891
|
+
|
|
1892
|
+
if hasattr(self, 'export_button') and self.export_button.menu():
|
|
1893
|
+
# Naive approach: check if separator/actions exist.
|
|
1894
|
+
# Better approach: Add them to a specific section or manage them explicitly.
|
|
1895
|
+
# Here we just append if not present.
|
|
1754
1896
|
|
|
1897
|
+
# Check if we need a separator (if we have built-in actions)
|
|
1898
|
+
if self.export_button.menu().actions():
|
|
1899
|
+
has_sep = False
|
|
1900
|
+
for a in self.export_button.menu().actions():
|
|
1901
|
+
if a.isSeparator():
|
|
1902
|
+
has_sep = True
|
|
1755
1903
|
# Get or create the parent menu for this plugin
|
|
1756
|
-
|
|
1904
|
+
# For folder based hierarchy only?
|
|
1905
|
+
# ... (Refer to existing code for menu building, skipped here for brevity)
|
|
1906
|
+
pass
|
|
1907
|
+
|
|
1908
|
+
# 5. Integrate File Openers into Import Menu
|
|
1909
|
+
if hasattr(self, 'import_menu') and self.plugin_manager.file_openers:
|
|
1910
|
+
# Add a separator if plugins are present
|
|
1911
|
+
has_plugins = len(self.plugin_manager.file_openers) > 0
|
|
1912
|
+
if has_plugins:
|
|
1913
|
+
self.import_menu.addSeparator()
|
|
1914
|
+
|
|
1915
|
+
for ext, info in self.plugin_manager.file_openers.items():
|
|
1916
|
+
# ext e.g. .xyz
|
|
1917
|
+
# info = {'plugin': name, 'callback': cb}
|
|
1918
|
+
label = f"Import {ext} ({info.get('plugin', 'Plugin')})..."
|
|
1919
|
+
|
|
1920
|
+
# duplicate check
|
|
1921
|
+
exists = False
|
|
1922
|
+
for act in self.import_menu.actions():
|
|
1923
|
+
if act.text() == label:
|
|
1924
|
+
exists = True
|
|
1925
|
+
break
|
|
1926
|
+
|
|
1927
|
+
if not exists:
|
|
1928
|
+
def make_cb(callback):
|
|
1929
|
+
def _cb():
|
|
1930
|
+
# Standard file dialog to pick file, then callback
|
|
1931
|
+
fpath, _ = QFileDialog.getOpenFileName(
|
|
1932
|
+
self, f"Import {ext}", "",
|
|
1933
|
+
f"{info.get('plugin', 'Plugin')} File (*{ext});;All Files (*)"
|
|
1934
|
+
)
|
|
1935
|
+
if fpath:
|
|
1936
|
+
callback(fpath)
|
|
1937
|
+
self.current_file_path = fpath # Update current file path?
|
|
1938
|
+
self.update_window_title()
|
|
1939
|
+
return _cb
|
|
1940
|
+
|
|
1941
|
+
a = QAction(label, self)
|
|
1942
|
+
a.triggered.connect(make_cb(info['callback']))
|
|
1943
|
+
self.import_menu.addAction(a)
|
|
1944
|
+
|
|
1945
|
+
# 6. Integrate Analysis Tools into Analysis Menu
|
|
1946
|
+
if hasattr(self, 'analysis_action') and self.plugin_manager.analysis_tools:
|
|
1947
|
+
# Determine parent menu (Analysis)
|
|
1948
|
+
# self.analysis_action is just an action, we need the menu it belongs to?
|
|
1949
|
+
# Or did we stash the analysis_menu?
|
|
1950
|
+
# Looking at init, analysis_menu wasn't stored as self.analysis_menu, but we can find it via menuBar.
|
|
1951
|
+
|
|
1952
|
+
# Let's find "Analysis" menu
|
|
1953
|
+
analysis_menu = None
|
|
1954
|
+
for action in self.menuBar().actions():
|
|
1955
|
+
if action.text().replace('&', '') == 'Analysis':
|
|
1956
|
+
analysis_menu = action.menu()
|
|
1957
|
+
break
|
|
1958
|
+
|
|
1959
|
+
if analysis_menu:
|
|
1960
|
+
# Add separator if we have plugins
|
|
1961
|
+
if self.plugin_manager.analysis_tools:
|
|
1962
|
+
analysis_menu.addSeparator()
|
|
1963
|
+
|
|
1964
|
+
for tool in self.plugin_manager.analysis_tools:
|
|
1965
|
+
label = f"{tool['label']} ({tool.get('plugin', 'Plugin')})"
|
|
1966
|
+
# duplicate check
|
|
1967
|
+
exists = False
|
|
1968
|
+
for act in analysis_menu.actions():
|
|
1969
|
+
if act.text() == label:
|
|
1970
|
+
exists = True
|
|
1971
|
+
break
|
|
1972
|
+
if not exists:
|
|
1973
|
+
a = QAction(label, self)
|
|
1974
|
+
a.triggered.connect(tool['callback'])
|
|
1975
|
+
analysis_menu.addAction(a)
|
|
1976
|
+
|
|
1977
|
+
# 7. Integrate Export Actions (Continued)
|
|
1978
|
+
if self.plugin_manager.export_actions:
|
|
1979
|
+
for exp in self.plugin_manager.export_actions:
|
|
1980
|
+
label = exp['label']
|
|
1981
|
+
callback = exp['callback']
|
|
1982
|
+
|
|
1983
|
+
exists = False
|
|
1984
|
+
for act in self.export_button.menu().actions():
|
|
1985
|
+
if act.text() == label:
|
|
1986
|
+
exists = True
|
|
1987
|
+
break
|
|
1757
1988
|
|
|
1758
|
-
if
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
if new_path not in menus:
|
|
1766
|
-
# Create new submenu
|
|
1767
|
-
sub_menu = parent_menu.addMenu(part)
|
|
1768
|
-
menus[new_path] = sub_menu
|
|
1769
|
-
|
|
1770
|
-
parent_menu = menus[new_path]
|
|
1771
|
-
current_path = new_path
|
|
1772
|
-
|
|
1773
|
-
# Add action to the resolved parent_menu
|
|
1774
|
-
action = QAction(p['name'], self)
|
|
1775
|
-
action.triggered.connect(lambda checked, mod=p['module']: self.plugin_manager.run_plugin(mod, self.mw if hasattr(self, 'mw') else self))
|
|
1776
|
-
parent_menu.addAction(action)
|
|
1989
|
+
if not exists:
|
|
1990
|
+
a = QAction(label, self)
|
|
1991
|
+
a.triggered.connect(callback)
|
|
1992
|
+
self.export_button.menu().addAction(a)
|
|
1993
|
+
|
|
1994
|
+
# 5. Integrate File Openers (Implicitly handled during file load) uses PluginManager directly
|
|
1777
1995
|
|
|
@@ -83,7 +83,7 @@ class MainWindowUiManager(object):
|
|
|
83
83
|
|
|
84
84
|
def __init__(self, main_window):
|
|
85
85
|
""" クラスの初期化 """
|
|
86
|
-
self
|
|
86
|
+
self = main_window
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
def update_status_bar(self, message):
|
|
@@ -330,6 +330,17 @@ class MainWindowUiManager(object):
|
|
|
330
330
|
continue
|
|
331
331
|
|
|
332
332
|
if file_path:
|
|
333
|
+
# 1. Custom Plugin Handlers
|
|
334
|
+
if self.plugin_manager and hasattr(self.plugin_manager, 'drop_handlers'):
|
|
335
|
+
for handler_def in self.plugin_manager.drop_handlers:
|
|
336
|
+
try:
|
|
337
|
+
callback = handler_def['callback']
|
|
338
|
+
handled = callback(file_path)
|
|
339
|
+
if handled:
|
|
340
|
+
event.acceptProposedAction()
|
|
341
|
+
return
|
|
342
|
+
except Exception as e:
|
|
343
|
+
print(f"Error in plugin drop handler: {e}")
|
|
333
344
|
# ドロップ位置を取得
|
|
334
345
|
drop_pos = event.position().toPoint()
|
|
335
346
|
# 拡張子に応じて適切な読み込みメソッドを呼び出す
|