MoleditPy 2.0.0__tar.gz → 2.1.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 (62) hide show
  1. {moleditpy-2.0.0 → moleditpy-2.1.0}/PKG-INFO +1 -1
  2. {moleditpy-2.0.0 → moleditpy-2.1.0}/pyproject.toml +1 -1
  3. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/constants.py +1 -1
  5. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_main_init.py +38 -2
  6. moleditpy-2.1.0/src/moleditpy/modules/plugin_manager.py +120 -0
  7. moleditpy-2.0.0/src/moleditpy/modules/plugin_manager.py +0 -85
  8. {moleditpy-2.0.0 → moleditpy-2.1.0}/LICENSE +0 -0
  9. {moleditpy-2.0.0 → moleditpy-2.1.0}/README.md +0 -0
  10. {moleditpy-2.0.0 → moleditpy-2.1.0}/setup.cfg +0 -0
  11. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  12. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  13. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  14. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/MoleditPy.egg-info/requires.txt +0 -0
  15. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/MoleditPy.egg-info/top_level.txt +0 -0
  16. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/__init__.py +0 -0
  17. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/__main__.py +0 -0
  18. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/main.py +0 -0
  19. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/__init__.py +0 -0
  20. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/about_dialog.py +0 -0
  21. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/align_plane_dialog.py +0 -0
  22. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/alignment_dialog.py +0 -0
  23. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/analysis_window.py +0 -0
  24. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/angle_dialog.py +0 -0
  25. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/assets/icon.icns +0 -0
  26. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/assets/icon.ico +0 -0
  27. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/assets/icon.png +0 -0
  28. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/atom_item.py +0 -0
  29. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/bond_item.py +0 -0
  30. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/bond_length_dialog.py +0 -0
  31. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/calculation_worker.py +0 -0
  32. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/color_settings_dialog.py +0 -0
  33. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
  34. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/custom_interactor_style.py +0 -0
  35. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
  36. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
  37. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/dihedral_dialog.py +0 -0
  38. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window.py +0 -0
  39. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_app_state.py +0 -0
  40. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_compute.py +0 -0
  41. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
  42. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
  43. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
  44. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_export.py +0 -0
  45. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
  46. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_project_io.py +0 -0
  47. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_string_importers.py +0 -0
  48. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_ui_manager.py +0 -0
  49. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_view_3d.py +0 -0
  50. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
  51. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/mirror_dialog.py +0 -0
  52. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/molecular_data.py +0 -0
  53. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/molecule_scene.py +0 -0
  54. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/move_group_dialog.py +0 -0
  55. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
  56. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/planarize_dialog.py +0 -0
  57. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/settings_dialog.py +0 -0
  58. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/template_preview_item.py +0 -0
  59. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/template_preview_view.py +0 -0
  60. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/translation_dialog.py +0 -0
  61. {moleditpy-2.0.0 → moleditpy-2.1.0}/src/moleditpy/modules/user_template_dialog.py +0 -0
  62. {moleditpy-2.0.0 → moleditpy-2.1.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.0.0
3
+ Version: 2.1.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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "2.0.0"
8
+ version = "2.1.0"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.0.0
3
+ Version: 2.1.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
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QFont, QColor
16
16
  from rdkit import Chem
17
17
 
18
18
  #Version
19
- VERSION = '2.0.0'
19
+ VERSION = '2.1.0'
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -1214,6 +1214,8 @@ class MainWindowMainInit(object):
1214
1214
  reload_plugins_action = QAction("Reload Plugins", self)
1215
1215
  reload_plugins_action.triggered.connect(lambda: self.update_plugin_menu(plugin_menu))
1216
1216
  plugin_menu.addAction(reload_plugins_action)
1217
+
1218
+
1217
1219
 
1218
1220
  plugin_menu.addSeparator()
1219
1221
 
@@ -1723,19 +1725,53 @@ class MainWindowMainInit(object):
1723
1725
  reload_plugins_action = QAction("Reload Plugins", self)
1724
1726
  reload_plugins_action.triggered.connect(lambda: self.update_plugin_menu(plugin_menu))
1725
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://github.com/HiroYokoyama/moleditpy-plugins"))
1732
+ )
1733
+ plugin_menu.addAction(explore_plugins_action)
1726
1734
 
1727
1735
  plugin_menu.addSeparator()
1728
1736
 
1729
1737
  # Add dynamic plugin actions
1730
1738
  plugins = self.plugin_manager.discover_plugins(self)
1739
+
1731
1740
  if not plugins:
1732
1741
  no_plugin_action = QAction("(No plugins found)", self)
1733
1742
  no_plugin_action.setEnabled(False)
1734
1743
  plugin_menu.addAction(no_plugin_action)
1735
1744
  else:
1745
+ # Sort plugins: directories first (to create menus), then alphabetical by name
1746
+ # Actually simple sort by rel_folder, name is fine
1747
+ plugins.sort(key=lambda x: (x.get('rel_folder', ''), x['name']))
1748
+
1749
+ # Dictionary to keep track of created submenus: path -> QMenu
1750
+ menus = { "": plugin_menu }
1751
+
1736
1752
  for p in plugins:
1737
- # Use default param in lambda to capture the current p
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
1738
1774
  action = QAction(p['name'], self)
1739
1775
  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)
1776
+ parent_menu.addAction(action)
1741
1777
 
@@ -0,0 +1,120 @@
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
+ Recursively scans the plugin directory for .py files and attempts to import them.
38
+ Ignores __pycache__ and other directories starting with "__".
39
+ Returns a list of valid loaded plugins.
40
+ """
41
+ self.ensure_plugin_dir()
42
+ self.plugins = []
43
+
44
+ if not os.path.exists(self.plugin_dir):
45
+ return []
46
+
47
+ for root, dirs, files in os.walk(self.plugin_dir):
48
+ # Modify dirs in-place to skip hidden directories and __pycache__
49
+ dirs[:] = [d for d in dirs if not d.startswith('__') and d != '__pycache__']
50
+
51
+ for filename in files:
52
+ if filename.endswith(".py") and not filename.startswith("__"):
53
+ filepath = os.path.join(root, filename)
54
+
55
+ # Calculate relative folder path for menu structure
56
+ # equivalent to: rel_path = os.path.relpath(root, self.plugin_dir)
57
+ # if root is plugin_dir, rel_path is '.'
58
+ rel_folder = os.path.relpath(root, self.plugin_dir)
59
+ if rel_folder == '.':
60
+ rel_folder = ""
61
+
62
+ try:
63
+ # Unique module name based on file path to avoid conflicts
64
+ # e.g. plugins.subdir.myplugin
65
+ module_name = os.path.splitext(os.path.relpath(filepath, self.plugin_dir))[0].replace(os.sep, '.')
66
+
67
+ spec = importlib.util.spec_from_file_location(module_name, filepath)
68
+ if spec and spec.loader:
69
+ module = importlib.util.module_from_spec(spec)
70
+ sys.modules[spec.name] = module
71
+ spec.loader.exec_module(module)
72
+
73
+ # Check for required attributes
74
+ plugin_name = getattr(module, 'PLUGIN_NAME', filename[:-3])
75
+
76
+ # Valid plugin if it has 'run' OR 'autorun'
77
+ has_run = hasattr(module, 'run') and callable(module.run)
78
+ has_autorun = hasattr(module, 'autorun') and callable(module.autorun)
79
+
80
+ if has_run:
81
+ self.plugins.append({
82
+ 'name': plugin_name,
83
+ 'module': module,
84
+ 'rel_folder': rel_folder
85
+ })
86
+
87
+ if has_autorun:
88
+ try:
89
+ if parent:
90
+ module.autorun(parent)
91
+ else:
92
+ print(f"Skipping autorun for {plugin_name}: parent not provided.")
93
+ except Exception as e:
94
+ print(f"Error executing autorun for {filename}: {e}")
95
+ traceback.print_exc()
96
+
97
+ if not has_run and not has_autorun:
98
+ print(f"Plugin {filename} skipped: Missing 'run(main_window)' or 'autorun(main_window)' function.")
99
+
100
+ except Exception as e:
101
+ # Robust error handling
102
+ msg = f"Failed to load plugin {filename}:\n{e}"
103
+ print(msg)
104
+ traceback.print_exc()
105
+ if parent:
106
+ # Use print/status bar instead of popups for non-critical failures during bulk load?
107
+ # For now, keep it visible but maybe less intrusive if many fail?
108
+ # sticking to original logic just in catch block
109
+ pass
110
+
111
+ return self.plugins
112
+
113
+ def run_plugin(self, module, main_window):
114
+ """Executes the plugin's run method."""
115
+ try:
116
+ module.run(main_window)
117
+ except Exception as e:
118
+ QMessageBox.critical(main_window, "Plugin Error", f"Error running plugin '{getattr(module, 'PLUGIN_NAME', 'Unknown')}':\n{e}")
119
+ traceback.print_exc()
120
+
@@ -1,85 +0,0 @@
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
-
File without changes
File without changes
File without changes