MoleditPy-linux 1.18.1__tar.gz → 2.0.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.
Files changed (61) hide show
  1. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/PKG-INFO +3 -1
  2. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/README.md +2 -0
  3. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/pyproject.toml +1 -1
  4. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/MoleditPy_linux.egg-info/PKG-INFO +3 -1
  5. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/MoleditPy_linux.egg-info/SOURCES.txt +1 -0
  6. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/constants.py +1 -1
  7. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window.py +4 -0
  8. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_main_init.py +65 -1
  9. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/molecule_scene.py +34 -1
  10. moleditpy_linux-2.0.0/src/moleditpy_linux/modules/plugin_manager.py +85 -0
  11. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/LICENSE +0 -0
  12. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/setup.cfg +0 -0
  13. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  14. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  15. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  16. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  17. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/__init__.py +0 -0
  18. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/__main__.py +0 -0
  19. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/main.py +0 -0
  20. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/__init__.py +0 -0
  21. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/about_dialog.py +0 -0
  22. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/align_plane_dialog.py +0 -0
  23. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/alignment_dialog.py +0 -0
  24. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/analysis_window.py +0 -0
  25. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/angle_dialog.py +0 -0
  26. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/assets/icon.icns +0 -0
  27. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/assets/icon.ico +0 -0
  28. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/assets/icon.png +0 -0
  29. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/atom_item.py +0 -0
  30. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/bond_item.py +0 -0
  31. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/bond_length_dialog.py +0 -0
  32. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/calculation_worker.py +0 -0
  33. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/color_settings_dialog.py +0 -0
  34. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/constrained_optimization_dialog.py +0 -0
  35. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/custom_interactor_style.py +0 -0
  36. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/custom_qt_interactor.py +0 -0
  37. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/dialog3_d_picking_mixin.py +0 -0
  38. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/dihedral_dialog.py +0 -0
  39. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_app_state.py +0 -0
  40. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_compute.py +0 -0
  41. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_dialog_manager.py +0 -0
  42. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_edit_3d.py +0 -0
  43. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_edit_actions.py +0 -0
  44. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_export.py +0 -0
  45. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_molecular_parsers.py +0 -0
  46. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_project_io.py +0 -0
  47. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_string_importers.py +0 -0
  48. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_ui_manager.py +0 -0
  49. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_view_3d.py +0 -0
  50. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/main_window_view_loaders.py +0 -0
  51. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/mirror_dialog.py +0 -0
  52. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/molecular_data.py +0 -0
  53. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/move_group_dialog.py +0 -0
  54. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/periodic_table_dialog.py +0 -0
  55. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/planarize_dialog.py +0 -0
  56. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/settings_dialog.py +0 -0
  57. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/template_preview_item.py +0 -0
  58. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/template_preview_view.py +0 -0
  59. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/translation_dialog.py +0 -0
  60. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/user_template_dialog.py +0 -0
  61. {moleditpy_linux-1.18.1 → moleditpy_linux-2.0.0}/src/moleditpy_linux/modules/zoomable_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 1.18.1
3
+ Version: 2.0.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
@@ -760,6 +760,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
760
760
  * Import structures from **MOL/SDF** files or **SMILES** strings.
761
761
  * Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
762
762
  * Export 2D and 3D views as high-resolution PNG images.
763
+ * **Plugin System:** Extend functionality with Python scripts. Place custom scripts in `~/.moleditpy/plugins` to add new features to the "Plugin" menu.
763
764
 
764
765
  ## Installation and Execution
765
766
 
@@ -874,6 +875,7 @@ This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**.
874
875
  * **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
875
876
  * 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
876
877
  * 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
878
+ * **プラグインシステム:** Pythonスクリプトで機能を拡張できます。`~/.moleditpy/plugins` にスクリプトを配置することで、「Plugin」メニューに独自の機能を追加できます。
877
879
 
878
880
  ## インストールと実行
879
881
 
@@ -59,6 +59,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
59
59
  * Import structures from **MOL/SDF** files or **SMILES** strings.
60
60
  * Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
61
61
  * Export 2D and 3D views as high-resolution PNG images.
62
+ * **Plugin System:** Extend functionality with Python scripts. Place custom scripts in `~/.moleditpy/plugins` to add new features to the "Plugin" menu.
62
63
 
63
64
  ## Installation and Execution
64
65
 
@@ -173,6 +174,7 @@ This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**.
173
174
  * **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
174
175
  * 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
175
176
  * 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
177
+ * **プラグインシステム:** Pythonスクリプトで機能を拡張できます。`~/.moleditpy/plugins` にスクリプトを配置することで、「Plugin」メニューに独自の機能を追加できます。
176
178
 
177
179
  ## インストールと実行
178
180
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "1.18.1"
8
+ version = "2.0.0"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 1.18.1
3
+ Version: 2.0.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
@@ -760,6 +760,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
760
760
  * Import structures from **MOL/SDF** files or **SMILES** strings.
761
761
  * Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
762
762
  * Export 2D and 3D views as high-resolution PNG images.
763
+ * **Plugin System:** Extend functionality with Python scripts. Place custom scripts in `~/.moleditpy/plugins` to add new features to the "Plugin" menu.
763
764
 
764
765
  ## Installation and Execution
765
766
 
@@ -874,6 +875,7 @@ This project is licensed under the **GNU General Public License v3.0 (GPL-v3)**.
874
875
  * **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
875
876
  * 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
876
877
  * 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
878
+ * **プラグインシステム:** Pythonスクリプトで機能を拡張できます。`~/.moleditpy/plugins` にスクリプトを配置することで、「Plugin」メニューに独自の機能を追加できます。
877
879
 
878
880
  ## インストールと実行
879
881
 
@@ -47,6 +47,7 @@ src/moleditpy_linux/modules/molecule_scene.py
47
47
  src/moleditpy_linux/modules/move_group_dialog.py
48
48
  src/moleditpy_linux/modules/periodic_table_dialog.py
49
49
  src/moleditpy_linux/modules/planarize_dialog.py
50
+ src/moleditpy_linux/modules/plugin_manager.py
50
51
  src/moleditpy_linux/modules/settings_dialog.py
51
52
  src/moleditpy_linux/modules/template_preview_item.py
52
53
  src/moleditpy_linux/modules/template_preview_view.py
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QFont, QColor
16
16
  from rdkit import Chem
17
17
 
18
18
  #Version
19
- VERSION = '1.18.1'
19
+ VERSION = '2.0.0'
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -211,6 +211,10 @@ class MainWindow(QMainWindow):
211
211
  # --- MOVED TO main_window_main_init.py ---
212
212
  return self.main_window_main_init.init_menu_bar()
213
213
 
214
+ def update_plugin_menu(self, plugin_menu):
215
+ # --- MOVED TO main_window_main_init.py ---
216
+ return self.main_window_main_init.update_plugin_menu(plugin_menu)
217
+
214
218
  def init_worker_thread(self):
215
219
  # --- MOVED TO main_window_main_init.py ---
216
220
  return self.main_window_main_init.init_worker_thread()
@@ -46,12 +46,16 @@ from PyQt6.QtCore import (
46
46
  Qt, QPointF, QRectF, QLineF, QUrl, QTimer
47
47
  )
48
48
  import platform
49
- import subprocess
50
49
  try:
51
50
  import winreg
52
51
  except Exception:
53
52
  winreg = None
54
53
 
54
+ try:
55
+ from .plugin_manager import PluginManager
56
+ except Exception:
57
+ from modules.plugin_manager import PluginManager
58
+
55
59
 
56
60
  def detect_system_dark_mode():
57
61
  """Return True if the OS prefers dark app theme, False if light, or None if unknown.
@@ -253,6 +257,14 @@ class MainWindowMainInit(object):
253
257
  # 3D編集ダイアログの参照を保持
254
258
  self.active_3d_dialogs = []
255
259
 
260
+
261
+ # プラグインマネージャーの初期化
262
+ try:
263
+ self.plugin_manager = PluginManager()
264
+ except Exception as e:
265
+ print(f"Failed to initialize PluginManager: {e}")
266
+ self.plugin_manager = None
267
+
256
268
  self.init_ui()
257
269
  self.init_worker_thread()
258
270
  self._setup_3d_picker()
@@ -286,6 +298,7 @@ class MainWindowMainInit(object):
286
298
  self.update_atom_id_menu_text()
287
299
  self.update_atom_id_menu_state()
288
300
 
301
+
289
302
  # 初期化完了を設定
290
303
  self.initialization_complete = True
291
304
  self.update_window_title() # 初期化完了後にタイトルを更新
@@ -1188,6 +1201,25 @@ class MainWindowMainInit(object):
1188
1201
  edit_3d_menu.addAction(constrained_opt_action)
1189
1202
  self.constrained_opt_action = constrained_opt_action
1190
1203
 
1204
+ # Plugin menu
1205
+ plugin_menu = menu_bar.addMenu("&Plugin")
1206
+
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)
1217
+
1218
+ plugin_menu.addSeparator()
1219
+
1220
+ # Initial population of plugins
1221
+ self.update_plugin_menu(plugin_menu)
1222
+
1191
1223
  settings_menu = menu_bar.addMenu("&Settings")
1192
1224
  # 1) 3D View settings (existing)
1193
1225
  view_settings_action = QAction("3D View Settings...", self)
@@ -1675,3 +1707,35 @@ class MainWindowMainInit(object):
1675
1707
  except Exception as e:
1676
1708
  print(f"Error saving settings: {e}")
1677
1709
 
1710
+ def update_plugin_menu(self, plugin_menu):
1711
+ """Discovers plugins and updates the plugin menu actions."""
1712
+ if not self.plugin_manager:
1713
+ return
1714
+
1715
+ # Clear existing plugin actions
1716
+ plugin_menu.clear()
1717
+
1718
+ # Re-add static actions
1719
+ open_plugin_dir_action = QAction("Open Plugin Directory", self)
1720
+ open_plugin_dir_action.triggered.connect(self.plugin_manager.open_plugin_folder)
1721
+ plugin_menu.addAction(open_plugin_dir_action)
1722
+
1723
+ reload_plugins_action = QAction("Reload Plugins", self)
1724
+ reload_plugins_action.triggered.connect(lambda: self.update_plugin_menu(plugin_menu))
1725
+ plugin_menu.addAction(reload_plugins_action)
1726
+
1727
+ plugin_menu.addSeparator()
1728
+
1729
+ # Add dynamic plugin actions
1730
+ plugins = self.plugin_manager.discover_plugins(self)
1731
+ if not plugins:
1732
+ no_plugin_action = QAction("(No plugins found)", self)
1733
+ no_plugin_action.setEnabled(False)
1734
+ plugin_menu.addAction(no_plugin_action)
1735
+ else:
1736
+ for p in plugins:
1737
+ # Use default param in lambda to capture the current p
1738
+ action = QAction(p['name'], self)
1739
+ action.triggered.connect(lambda checked, mod=p['module']: self.plugin_manager.run_plugin(mod, self.mw if hasattr(self, 'mw') else self))
1740
+ plugin_menu.addAction(action)
1741
+
@@ -115,6 +115,15 @@ class MoleculeScene(QGraphicsScene):
115
115
  }
116
116
  self.reinitialize_items()
117
117
 
118
+
119
+ def update_all_items(self):
120
+ """全てのアイテムを強制的に再描画する"""
121
+ for item in self.items():
122
+ if isinstance(item, (AtomItem, BondItem)):
123
+ item.update()
124
+ if self.views():
125
+ self.views()[0].viewport().update()
126
+
118
127
  def reinitialize_items(self):
119
128
  self.template_preview = TemplatePreviewItem(); self.addItem(self.template_preview)
120
129
  self.template_preview.hide(); self.template_preview_points = []; self.template_context = {}
@@ -185,6 +194,7 @@ class MoleculeScene(QGraphicsScene):
185
194
  # Delete the entire rectangular selection
186
195
  data_changed = self.delete_items(set(selected_items))
187
196
  if data_changed:
197
+ self.update_all_items()
188
198
  self.window.push_undo_state()
189
199
  self.press_pos = None
190
200
  event.accept()
@@ -207,6 +217,7 @@ class MoleculeScene(QGraphicsScene):
207
217
  logging.error(f"Error clearing E/Z label: {e}", exc_info=True)
208
218
  if hasattr(self.window, 'statusBar'):
209
219
  self.window.statusBar().showMessage(f"Error clearing E/Z label: {e}", 5000)
220
+ self.update_all_items() # エラー時も整合性維持のため再描画
210
221
  # AtomItemは何もしない
211
222
  # --- 通常の処理 ---
212
223
  elif isinstance(item, AtomItem):
@@ -233,6 +244,7 @@ class MoleculeScene(QGraphicsScene):
233
244
  data_changed = self.delete_items({item})
234
245
 
235
246
  if data_changed:
247
+ self.update_all_items()
236
248
  self.window.push_undo_state()
237
249
  self.press_pos = None
238
250
  event.accept()
@@ -342,7 +354,9 @@ class MoleculeScene(QGraphicsScene):
342
354
  self.data_changed_in_event = True
343
355
  # イベント処理をここで完了させ、下のアイテムが選択されるのを防ぐ
344
356
  self.start_atom=None; self.start_pos = None; self.press_pos = None
345
- if self.data_changed_in_event: self.window.push_undo_state()
357
+ if self.data_changed_in_event:
358
+ self.update_all_items()
359
+ self.window.push_undo_state()
346
360
  return
347
361
 
348
362
  released_item = self.itemAt(end_pos, self.views()[0].transform())
@@ -384,11 +398,13 @@ class MoleculeScene(QGraphicsScene):
384
398
  else: # current_stereo == 4
385
399
  new_stereo = 0 # E -> None
386
400
  self.update_bond_stereo(b, new_stereo)
401
+ self.update_all_items() # 強制再描画
387
402
  self.window.push_undo_state() # ここでUndo stackに積む
388
403
  except Exception as e:
389
404
  logging.error(f"Error in E/Z stereo toggle: {e}", exc_info=True)
390
405
  if hasattr(self.window, 'statusBar'):
391
406
  self.window.statusBar().showMessage(f"Error changing E/Z stereochemistry: {e}", 5000)
407
+ self.update_all_items() # エラー時も整合性維持のため再描画
392
408
  return # この後の処理は行わない
393
409
  elif self.bond_stereo != 0 and b.order == self.bond_order and b.stereo == self.bond_stereo:
394
410
  # 方向性を反転させる
@@ -488,6 +504,10 @@ class MoleculeScene(QGraphicsScene):
488
504
  # 原子移動後に測定ラベルの位置を更新
489
505
  self.window.update_2d_measurement_labels()
490
506
  if self.views(): self.views()[0].viewport().update()
507
+
508
+ if self.data_changed_in_event:
509
+ self.update_all_items()
510
+
491
511
  self.start_atom=None; self.start_pos = None; self.press_pos = None; self.temp_line = None
492
512
  self.template_context = {}
493
513
  # Clear user template data when switching modes
@@ -512,6 +532,7 @@ class MoleculeScene(QGraphicsScene):
512
532
  self.data.atoms[item.atom_id]['charge'] = item.charge
513
533
  item.update_style()
514
534
 
535
+ self.update_all_items()
515
536
  self.window.push_undo_state()
516
537
 
517
538
  event.accept()
@@ -635,6 +656,7 @@ class MoleculeScene(QGraphicsScene):
635
656
 
636
657
  except Exception as e:
637
658
  logging.error(f"Error creating bond: {e}", exc_info=True)
659
+ self.update_all_items() # エラーリカバリー
638
660
 
639
661
  def add_molecule_fragment(self, points, bonds_info, existing_items=None, symbol='C'):
640
662
  """
@@ -1265,6 +1287,7 @@ class MoleculeScene(QGraphicsScene):
1265
1287
  print(f"Error during delete_items operation: {e}")
1266
1288
 
1267
1289
  traceback.print_exc()
1290
+ self.update_all_items() # エラーリカバリー
1268
1291
  return False
1269
1292
  def purge_deleted_items(self):
1270
1293
  """Purge and release any held deleted-wrapper references.
@@ -1536,6 +1559,7 @@ class MoleculeScene(QGraphicsScene):
1536
1559
 
1537
1560
  # 計算した情報を使って、その場にフラグメントを追加
1538
1561
  self.add_molecule_fragment(points, bonds_info, existing_items=existing_items)
1562
+ self.update_all_items()
1539
1563
  self.window.push_undo_state()
1540
1564
 
1541
1565
  # --- 動作2: カーソルが空白領域にある場合 (モード切替) ---
@@ -1561,6 +1585,7 @@ class MoleculeScene(QGraphicsScene):
1561
1585
  atom.radical = (atom.radical + 1) % 3
1562
1586
  self.data.atoms[atom.atom_id]['radical'] = atom.radical
1563
1587
  atom.update_style()
1588
+ self.update_all_items()
1564
1589
  self.window.push_undo_state()
1565
1590
  event.accept()
1566
1591
  return
@@ -1581,6 +1606,7 @@ class MoleculeScene(QGraphicsScene):
1581
1606
  atom.charge += delta
1582
1607
  self.data.atoms[atom.atom_id]['charge'] = atom.charge
1583
1608
  atom.update_style()
1609
+ self.update_all_items()
1584
1610
  self.window.push_undo_state()
1585
1611
  event.accept()
1586
1612
  return
@@ -1610,6 +1636,7 @@ class MoleculeScene(QGraphicsScene):
1610
1636
  for atom in atoms_to_update:
1611
1637
  atom.update_style()
1612
1638
 
1639
+ self.update_all_items()
1613
1640
  self.window.push_undo_state()
1614
1641
  event.accept()
1615
1642
  return
@@ -1688,6 +1715,7 @@ class MoleculeScene(QGraphicsScene):
1688
1715
  bond.update()
1689
1716
 
1690
1717
  if any_bond_changed:
1718
+ self.update_all_items()
1691
1719
  self.window.push_undo_state()
1692
1720
 
1693
1721
  if key in [Qt.Key.Key_1, Qt.Key.Key_2, Qt.Key.Key_3, Qt.Key.Key_W, Qt.Key.Key_D]:
@@ -1697,11 +1725,13 @@ class MoleculeScene(QGraphicsScene):
1697
1725
  if isinstance(self.hovered_item, BondItem) and self.hovered_item.order == 2:
1698
1726
  if event.key() == Qt.Key.Key_Z:
1699
1727
  self.update_bond_stereo(self.hovered_item, 3) # Z-isomer
1728
+ self.update_all_items()
1700
1729
  self.window.push_undo_state()
1701
1730
  event.accept()
1702
1731
  return
1703
1732
  elif event.key() == Qt.Key.Key_E:
1704
1733
  self.update_bond_stereo(self.hovered_item, 4) # E-isomer
1734
+ self.update_all_items()
1705
1735
  self.window.push_undo_state()
1706
1736
  event.accept()
1707
1737
  return
@@ -1809,6 +1839,7 @@ class MoleculeScene(QGraphicsScene):
1809
1839
  self.create_bond(start_atom, new_atom_item, bond_order=target_order, bond_stereo=0)
1810
1840
 
1811
1841
  self.clearSelection()
1842
+ self.update_all_items()
1812
1843
  self.window.push_undo_state()
1813
1844
  event.accept()
1814
1845
  return
@@ -1839,6 +1870,7 @@ class MoleculeScene(QGraphicsScene):
1839
1870
  items_to_process.add(item_at_cursor)
1840
1871
 
1841
1872
  if self.delete_items(items_to_process):
1873
+ self.update_all_items()
1842
1874
  self.window.push_undo_state()
1843
1875
  self.window.statusBar().showMessage("Deleted selected items.")
1844
1876
 
@@ -1973,3 +2005,4 @@ class MoleculeScene(QGraphicsScene):
1973
2005
  traceback.print_exc()
1974
2006
  if hasattr(self.window, 'statusBar'):
1975
2007
  self.window.statusBar().showMessage(f"Error updating bond stereochemistry: {e}", 5000)
2008
+ self.update_all_items() # エラーリカバリー
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ plugin_manager.py
6
+ Manages discovery, loading, and execution of external plugins.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import importlib.util
12
+ import traceback
13
+ from PyQt6.QtGui import QDesktopServices
14
+ from PyQt6.QtCore import QUrl
15
+ from PyQt6.QtWidgets import QMessageBox
16
+
17
+ class PluginManager:
18
+ def __init__(self):
19
+ self.plugin_dir = os.path.join(os.path.expanduser('~'), '.moleditpy', 'plugins')
20
+ self.plugins = [] # List of {"name": str, "module": module_obj}
21
+
22
+ def ensure_plugin_dir(self):
23
+ """Creates the plugin directory if it creates doesn't exist."""
24
+ if not os.path.exists(self.plugin_dir):
25
+ try:
26
+ os.makedirs(self.plugin_dir)
27
+ except OSError as e:
28
+ print(f"Error creating plugin directory: {e}")
29
+
30
+ def open_plugin_folder(self):
31
+ """Opens the plugin directory in the OS file explorer."""
32
+ self.ensure_plugin_dir()
33
+ QDesktopServices.openUrl(QUrl.fromLocalFile(self.plugin_dir))
34
+
35
+ def discover_plugins(self, parent=None):
36
+ """
37
+ Scans the plugin directory for .py files and attempts to import them.
38
+ Returns a list of valid loaded plugins.
39
+ """
40
+ self.ensure_plugin_dir()
41
+ self.plugins = []
42
+
43
+ if not os.path.exists(self.plugin_dir):
44
+ return []
45
+
46
+ for filename in os.listdir(self.plugin_dir):
47
+ if filename.endswith(".py") and not filename.startswith("__"):
48
+ filepath = os.path.join(self.plugin_dir, filename)
49
+ try:
50
+ # Dynamically import the module
51
+ spec = importlib.util.spec_from_file_location(filename[:-3], filepath)
52
+ if spec and spec.loader:
53
+ module = importlib.util.module_from_spec(spec)
54
+ sys.modules[spec.name] = module # helper for relative imports if needed
55
+ spec.loader.exec_module(module)
56
+
57
+ # Check for required attributes
58
+ plugin_name = getattr(module, 'PLUGIN_NAME', filename[:-3])
59
+
60
+ # Validate that it has a run function
61
+ if hasattr(module, 'run') and callable(module.run):
62
+ self.plugins.append({
63
+ 'name': plugin_name,
64
+ 'module': module
65
+ })
66
+ else:
67
+ print(f"Plugin {filename} skipped: Missing 'run(main_window)' function.")
68
+ except Exception as e:
69
+ # Robust error handling with user notification
70
+ msg = f"Failed to load plugin {filename}:\n{e}"
71
+ print(msg)
72
+ traceback.print_exc()
73
+ if parent:
74
+ QMessageBox.warning(parent, "Plugin Load Error", msg)
75
+
76
+ return self.plugins
77
+
78
+ def run_plugin(self, module, main_window):
79
+ """Executes the plugin's run method."""
80
+ try:
81
+ module.run(main_window)
82
+ except Exception as e:
83
+ QMessageBox.critical(main_window, "Plugin Error", f"Error running plugin '{getattr(module, 'PLUGIN_NAME', 'Unknown')}':\n{e}")
84
+ traceback.print_exc()
85
+