MoleditPy-linux 2.1.1__py3-none-any.whl → 2.2.0__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.
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QFont, QColor
16
16
  from rdkit import Chem
17
17
 
18
18
  #Version
19
- VERSION = '2.1.1'
19
+ VERSION = '2.2.0'
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -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
@@ -32,7 +32,7 @@ except Exception:
32
32
  # PyQt6 Modules
33
33
  from PyQt6.QtWidgets import (
34
34
  QApplication, QWidget, QVBoxLayout, QHBoxLayout,
35
- QPushButton, QSplitter, QToolBar, QSizePolicy, QLabel, QToolButton, QMenu, QMessageBox
35
+ QPushButton, QSplitter, QToolBar, QSizePolicy, QLabel, QToolButton, QMenu, QMessageBox, QFileDialog
36
36
  )
37
37
 
38
38
  from PyQt6.QtGui import (
@@ -328,7 +328,7 @@ class MainWindowMainInit(object):
328
328
  else:
329
329
  print(f"警告: アイコンファイルが見つかりません: {icon_path}")
330
330
 
331
- self.init_menu_bar()
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
- open_plugin_dir_action = QAction("Open Plugin Directory", self)
1208
- if self.plugin_manager:
1209
- open_plugin_dir_action.triggered.connect(self.plugin_manager.open_plugin_folder)
1210
- else:
1211
- open_plugin_dir_action.setEnabled(False)
1212
- plugin_menu.addAction(open_plugin_dir_action)
1213
-
1214
- reload_plugins_action = QAction("Reload Plugins", self)
1215
- reload_plugins_action.triggered.connect(lambda: self.update_plugin_menu(plugin_menu))
1216
- plugin_menu.addAction(reload_plugins_action)
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
- file_ext = file_path.lower().split('.')[-1]
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)
@@ -1713,65 +1755,229 @@ class MainWindowMainInit(object):
1713
1755
  """Discovers plugins and updates the plugin menu actions."""
1714
1756
  if not self.plugin_manager:
1715
1757
  return
1758
+
1759
+ PLUGIN_ACTION_TAG = "plugin_managed"
1760
+
1761
+ # Helper to clear tagged actions from a menu
1762
+ def clear_plugin_actions(menu):
1763
+ if not menu: return
1764
+ for act in list(menu.actions()):
1765
+ if act.data() == PLUGIN_ACTION_TAG:
1766
+ menu.removeAction(act)
1767
+ # Recurse into submenus to clean deep actions
1768
+ elif act.menu():
1769
+ clear_plugin_actions(act.menu())
1716
1770
 
1717
- # Clear existing plugin actions
1771
+ # Clear existing plugin actions from main Plugin menu
1718
1772
  plugin_menu.clear()
1719
1773
 
1720
- # Re-add static actions
1721
- open_plugin_dir_action = QAction("Open Plugin Directory", self)
1722
- open_plugin_dir_action.triggered.connect(self.plugin_manager.open_plugin_folder)
1723
- plugin_menu.addAction(open_plugin_dir_action)
1724
-
1725
- reload_plugins_action = QAction("Reload Plugins", self)
1726
- reload_plugins_action.triggered.connect(lambda: self.update_plugin_menu(plugin_menu))
1727
- plugin_menu.addAction(reload_plugins_action)
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)
1774
+ # Clear tagged actions from ALL top-level menus in the Menu Bar
1775
+ # This ensures we catch actions added to standard menus (File, Edit) OR custom menus
1776
+ for top_action in self.menuBar().actions():
1777
+ if top_action.menu():
1778
+ clear_plugin_actions(top_action.menu())
1779
+
1780
+ # Clear Export menu (if button exists)
1781
+ if hasattr(self, 'export_button') and self.export_button.menu():
1782
+ clear_plugin_actions(self.export_button.menu())
1783
+
1784
+ # Only keep the Manager action
1785
+ manage_plugins_action = QAction("Plugin Manager...", self)
1786
+ def show_plugin_manager():
1787
+ from .plugin_manager_window import PluginManagerWindow
1788
+ dlg = PluginManagerWindow(self.plugin_manager, self)
1789
+ dlg.exec()
1790
+ self.update_plugin_menu(plugin_menu) # Refresh after closing
1791
+ manage_plugins_action.triggered.connect(show_plugin_manager)
1792
+ plugin_menu.addAction(manage_plugins_action)
1734
1793
 
1735
1794
  plugin_menu.addSeparator()
1736
1795
 
1737
- # Add dynamic plugin actions
1796
+ # Add dynamic plugin actions (Legacy + New Registration)
1738
1797
  plugins = self.plugin_manager.discover_plugins(self)
1739
1798
 
1799
+ # 1. Add Registered Menu Actions (New System)
1800
+ if self.plugin_manager.menu_actions:
1801
+ for action_def in self.plugin_manager.menu_actions:
1802
+ path = action_def['path']
1803
+ callback = action_def['callback']
1804
+ text = action_def['text']
1805
+ # Create/Find menu path
1806
+ current_menu = self.menuBar() # Or find specific top-level
1807
+
1808
+ # Handling top-level menus vs nested
1809
+ parts = path.split('/')
1810
+
1811
+ # If path starts with existing top-level (File, Edit, etc), grab it
1812
+ # Otherwise create new top-level
1813
+ top_level_title = parts[0]
1814
+ found_top = False
1815
+ for act in self.menuBar().actions():
1816
+ if act.menu() and act.text().replace('&', '') == top_level_title:
1817
+ current_menu = act.menu()
1818
+ found_top = True
1819
+ break
1820
+
1821
+ if not found_top:
1822
+ current_menu = self.menuBar().addMenu(top_level_title)
1823
+
1824
+ # Traverse rest
1825
+ for part in parts[1:]:
1826
+ found_sub = False
1827
+ for act in current_menu.actions():
1828
+ if act.menu() and act.text().replace('&', '') == part:
1829
+ current_menu = act.menu()
1830
+ found_sub = True
1831
+ break
1832
+ if not found_sub:
1833
+ current_menu = current_menu.addMenu(part)
1834
+
1835
+ # Add action
1836
+ action_text = text if text else parts[-1]
1837
+ action = QAction(action_text, self)
1838
+ action.triggered.connect(callback)
1839
+ action.setData(PLUGIN_ACTION_TAG) # TAG THE ACTION
1840
+ current_menu.addAction(action)
1841
+
1842
+ # 2. Add Toolbar Buttons (New System)
1843
+ # Use dedicated plugin toolbar
1844
+ if hasattr(self, 'plugin_toolbar'):
1845
+ self.plugin_toolbar.clear()
1846
+
1847
+ if self.plugin_manager.toolbar_actions:
1848
+ self.plugin_toolbar.show()
1849
+ for action_def in self.plugin_manager.toolbar_actions:
1850
+ text = action_def['text']
1851
+ callback = action_def['callback']
1852
+
1853
+ action = QAction(text, self)
1854
+ action.triggered.connect(callback)
1855
+ if action_def['icon']:
1856
+ if os.path.exists(action_def['icon']):
1857
+ action.setIcon(QIcon(action_def['icon']))
1858
+ if action_def['tooltip']:
1859
+ action.setToolTip(action_def['tooltip'])
1860
+ self.plugin_toolbar.addAction(action)
1861
+ else:
1862
+ self.plugin_toolbar.hide()
1863
+
1864
+ # 3. Legacy Menu Building (Folder based)
1740
1865
  if not plugins:
1741
1866
  no_plugin_action = QAction("(No plugins found)", self)
1742
1867
  no_plugin_action.setEnabled(False)
1743
1868
  plugin_menu.addAction(no_plugin_action)
1744
1869
  else:
1745
1870
  # Sort plugins: directories first (to create menus), then alphabetical by name
1746
- # Actually simple sort by rel_folder, name is fine
1747
1871
  plugins.sort(key=lambda x: (x.get('rel_folder', ''), x['name']))
1748
1872
 
1749
1873
  # Dictionary to keep track of created submenus: path -> QMenu
1750
1874
  menus = { "": plugin_menu }
1751
1875
 
1752
1876
  for p in plugins:
1753
- rel_folder = p.get('rel_folder', "")
1754
-
1755
- # Get or create the parent menu for this plugin
1756
- parent_menu = menus.get("") # Start at root
1757
-
1758
- if rel_folder:
1759
- # Split path and traverse/create submenus
1760
- parts = rel_folder.split(os.sep)
1761
- current_path = ""
1762
- for part in parts:
1763
- new_path = os.path.join(current_path, part) if current_path else part
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)
1877
+ # Only add legacy plugins (with 'run' function) to the generic Plugins menu.
1878
+ if hasattr(p['module'], 'run'):
1879
+ rel_folder = p.get('rel_folder', '')
1880
+ # Get or create the parent menu for this plugin
1881
+ parent_menu = menus.get("") # Start at root
1882
+
1883
+ if rel_folder:
1884
+ # Split path and traverse/create submenus
1885
+ parts = rel_folder.split(os.sep)
1886
+ current_path = ""
1887
+ for part in parts:
1888
+ new_path = os.path.join(current_path, part) if current_path else part
1889
+
1890
+ if new_path not in menus:
1891
+ # Create new submenu
1892
+ sub_menu = parent_menu.addMenu(part)
1893
+ menus[new_path] = sub_menu
1894
+
1895
+ parent_menu = menus[new_path]
1896
+ current_path = new_path
1897
+
1898
+ # Add action to the resolved parent_menu
1899
+ action = QAction(p['name'], self)
1900
+ action.triggered.connect(lambda checked, mod=p['module']: self.plugin_manager.run_plugin(mod, self.mw if hasattr(self, 'mw') else self))
1901
+ parent_menu.addAction(action)
1902
+
1903
+ # 4. Integrate Export Actions into Export Button and Menu
1904
+ # 4. Integrate Export Actions into Export Button AND Main File->Export Menu
1905
+ if self.plugin_manager.export_actions:
1906
+ # Find Main File -> Export menu
1907
+ main_export_menu = None
1908
+ for top_action in self.menuBar().actions():
1909
+ if top_action.text().replace('&', '') == 'File' and top_action.menu():
1910
+ for sub_action in top_action.menu().actions():
1911
+ if sub_action.text().replace('&', '') == 'Export' and sub_action.menu():
1912
+ main_export_menu = sub_action.menu()
1913
+ break
1914
+ if main_export_menu: break
1915
+
1916
+ # List of menus to populate
1917
+ target_menus = []
1918
+ if hasattr(self, 'export_button') and self.export_button.menu():
1919
+ target_menus.append(self.export_button.menu())
1920
+ if main_export_menu:
1921
+ target_menus.append(main_export_menu)
1922
+
1923
+ for menu in target_menus:
1924
+ # Add separator
1925
+ sep = menu.addSeparator()
1926
+ sep.setData(PLUGIN_ACTION_TAG)
1927
+
1928
+ for exp in self.plugin_manager.export_actions:
1929
+ label = exp['label']
1930
+ callback = exp['callback']
1931
+
1932
+ a = QAction(label, self)
1933
+ a.triggered.connect(callback)
1934
+ a.setData(PLUGIN_ACTION_TAG)
1935
+ menu.addAction(a)
1936
+
1937
+ # 5. Integrate File Openers into Import Menu
1938
+ if hasattr(self, 'import_menu') and self.plugin_manager.file_openers:
1939
+ # Add separator
1940
+ sep = self.import_menu.addSeparator()
1941
+ sep.setData(PLUGIN_ACTION_TAG)
1942
+
1943
+ for ext, info in self.plugin_manager.file_openers.items():
1944
+ label = f"Import {ext} ({info.get('plugin', 'Plugin')})..."
1945
+
1946
+ def make_cb(callback):
1947
+ def _cb():
1948
+ fpath, _ = QFileDialog.getOpenFileName(
1949
+ self, f"Import {ext}", "",
1950
+ f"{info.get('plugin', 'Plugin')} File (*{ext});;All Files (*)"
1951
+ )
1952
+ if fpath:
1953
+ callback(fpath)
1954
+ self.current_file_path = fpath
1955
+ self.update_window_title()
1956
+ return _cb
1957
+
1958
+ a = QAction(label, self)
1959
+ a.triggered.connect(make_cb(info['callback']))
1960
+ a.setData(PLUGIN_ACTION_TAG)
1961
+ self.import_menu.addAction(a)
1962
+
1963
+ # 6. Integrate Analysis Tools into Analysis Menu
1964
+ # Find Analysis menu again as it might not be defined if cleanup block was generic
1965
+ analysis_menu = None
1966
+ for action in self.menuBar().actions():
1967
+ if action.text().replace('&', '') == 'Analysis':
1968
+ analysis_menu = action.menu()
1969
+ break
1970
+
1971
+ if analysis_menu and self.plugin_manager.analysis_tools:
1972
+ # Add separator
1973
+ sep = analysis_menu.addSeparator()
1974
+ sep.setData(PLUGIN_ACTION_TAG)
1975
+
1976
+ for tool in self.plugin_manager.analysis_tools:
1977
+ label = f"{tool['label']} ({tool.get('plugin', 'Plugin')})"
1978
+
1979
+ a = QAction(label, self)
1980
+ a.triggered.connect(tool['callback'])
1981
+ a.setData(PLUGIN_ACTION_TAG)
1982
+ analysis_menu.addAction(a)
1777
1983
 
@@ -83,7 +83,7 @@ class MainWindowUiManager(object):
83
83
 
84
84
  def __init__(self, main_window):
85
85
  """ クラスの初期化 """
86
- self.mw = main_window
86
+ self = main_window
87
87
 
88
88
 
89
89
  def update_status_bar(self, message):
@@ -298,7 +298,7 @@ class MainWindowUiManager(object):
298
298
 
299
299
 
300
300
  def dragEnterEvent(self, event):
301
- """ウィンドウ全体で .pmeraw、.pmeprj、.mol、.sdf、.xyz ファイルのドラッグを受け入れる"""
301
+ """ウィンドウ全体でサポートされているファイルのドラッグを受け入れる"""
302
302
  # Accept if any dragged local file has a supported extension
303
303
  if event.mimeData().hasUrls():
304
304
  urls = event.mimeData().urls()
@@ -306,9 +306,28 @@ class MainWindowUiManager(object):
306
306
  try:
307
307
  if url.isLocalFile():
308
308
  file_path = url.toLocalFile()
309
- if file_path.lower().endswith(('.pmeraw', '.pmeprj', '.mol', '.sdf', '.xyz')):
309
+ file_lower = file_path.lower()
310
+
311
+ # Built-in extensions
312
+ if file_lower.endswith(('.pmeraw', '.pmeprj', '.mol', '.sdf', '.xyz')):
310
313
  event.acceptProposedAction()
311
314
  return
315
+
316
+ # Plugin-registered file openers
317
+ if self.plugin_manager and hasattr(self.plugin_manager, 'file_openers'):
318
+ for ext in self.plugin_manager.file_openers.keys():
319
+ if file_lower.endswith(ext):
320
+ event.acceptProposedAction()
321
+ return
322
+
323
+ # Plugin drop handlers (accept more liberally for custom logic)
324
+ # A plugin drop handler might handle it, so accept
325
+ if self.plugin_manager and hasattr(self.plugin_manager, 'drop_handlers'):
326
+ if len(self.plugin_manager.drop_handlers) > 0:
327
+ # Accept any file if drop handlers are registered
328
+ # They will check the file type in dropEvent
329
+ event.acceptProposedAction()
330
+ return
312
331
  except Exception:
313
332
  continue
314
333
  event.ignore()
@@ -330,6 +349,17 @@ class MainWindowUiManager(object):
330
349
  continue
331
350
 
332
351
  if file_path:
352
+ # 1. Custom Plugin Handlers
353
+ if self.plugin_manager and hasattr(self.plugin_manager, 'drop_handlers'):
354
+ for handler_def in self.plugin_manager.drop_handlers:
355
+ try:
356
+ callback = handler_def['callback']
357
+ handled = callback(file_path)
358
+ if handled:
359
+ event.acceptProposedAction()
360
+ return
361
+ except Exception as e:
362
+ print(f"Error in plugin drop handler: {e}")
333
363
  # ドロップ位置を取得
334
364
  drop_pos = event.position().toPoint()
335
365
  # 拡張子に応じて適切な読み込みメソッドを呼び出す