MoleditPy 2.1.1__tar.gz → 2.2.0__tar.gz
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-2.1.1 → moleditpy-2.2.0}/PKG-INFO +1 -1
- {moleditpy-2.1.1 → moleditpy-2.2.0}/pyproject.toml +1 -1
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/MoleditPy.egg-info/PKG-INFO +1 -1
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/MoleditPy.egg-info/SOURCES.txt +2 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/constants.py +1 -1
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window.py +8 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_app_state.py +24 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_compute.py +27 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_main_init.py +268 -62
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_ui_manager.py +33 -3
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_view_3d.py +122 -35
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/molecule_scene.py +6 -14
- moleditpy-2.2.0/src/moleditpy/modules/plugin_interface.py +195 -0
- moleditpy-2.2.0/src/moleditpy/modules/plugin_manager.py +309 -0
- moleditpy-2.2.0/src/moleditpy/modules/plugin_manager_window.py +218 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/template_preview_item.py +3 -6
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/user_template_dialog.py +59 -1
- moleditpy-2.1.1/src/moleditpy/modules/plugin_manager.py +0 -120
- {moleditpy-2.1.1 → moleditpy-2.2.0}/LICENSE +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/README.md +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/setup.cfg +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/__init__.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/__main__.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/main.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/__init__.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/about_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/align_plane_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/alignment_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/analysis_window.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/angle_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/assets/icon.icns +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/assets/icon.ico +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/assets/icon.png +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/atom_item.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/bond_item.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/bond_length_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/calculation_worker.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/color_settings_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/custom_interactor_style.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/dihedral_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_export.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_project_io.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_string_importers.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/mirror_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/molecular_data.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/move_group_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/planarize_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/settings_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/template_preview_view.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/translation_dialog.py +0 -0
- {moleditpy-2.1.1 → moleditpy-2.2.0}/src/moleditpy/modules/zoomable_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
|
|
5
5
|
Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -53,7 +53,9 @@ src/moleditpy/modules/molecule_scene.py
|
|
|
53
53
|
src/moleditpy/modules/move_group_dialog.py
|
|
54
54
|
src/moleditpy/modules/periodic_table_dialog.py
|
|
55
55
|
src/moleditpy/modules/planarize_dialog.py
|
|
56
|
+
src/moleditpy/modules/plugin_interface.py
|
|
56
57
|
src/moleditpy/modules/plugin_manager.py
|
|
58
|
+
src/moleditpy/modules/plugin_manager_window.py
|
|
57
59
|
src/moleditpy/modules/settings_dialog.py
|
|
58
60
|
src/moleditpy/modules/template_preview_item.py
|
|
59
61
|
src/moleditpy/modules/template_preview_view.py
|
|
@@ -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
|
-
|
|
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)
|
|
@@ -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
|
-
#
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
)
|
|
1733
|
-
|
|
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
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
# 拡張子に応じて適切な読み込みメソッドを呼び出す
|