MoleditPy-linux 2.2.4__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.
Files changed (58) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +293 -0
  7. moleditpy_linux/modules/alignment_dialog.py +273 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/icon.icns +0 -0
  11. moleditpy_linux/modules/assets/icon.ico +0 -0
  12. moleditpy_linux/modules/assets/icon.png +0 -0
  13. moleditpy_linux/modules/atom_item.py +348 -0
  14. moleditpy_linux/modules/bond_item.py +406 -0
  15. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  16. moleditpy_linux/modules/calculation_worker.py +766 -0
  17. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  18. moleditpy_linux/modules/constants.py +88 -0
  19. moleditpy_linux/modules/constrained_optimization_dialog.py +679 -0
  20. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  21. moleditpy_linux/modules/custom_qt_interactor.py +59 -0
  22. moleditpy_linux/modules/dialog3_d_picking_mixin.py +108 -0
  23. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  24. moleditpy_linux/modules/main_window.py +842 -0
  25. moleditpy_linux/modules/main_window_app_state.py +780 -0
  26. moleditpy_linux/modules/main_window_compute.py +1242 -0
  27. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  28. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  29. moleditpy_linux/modules/main_window_edit_actions.py +1455 -0
  30. moleditpy_linux/modules/main_window_export.py +806 -0
  31. moleditpy_linux/modules/main_window_main_init.py +2006 -0
  32. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  33. moleditpy_linux/modules/main_window_project_io.py +434 -0
  34. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  35. moleditpy_linux/modules/main_window_ui_manager.py +606 -0
  36. moleditpy_linux/modules/main_window_view_3d.py +1531 -0
  37. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  38. moleditpy_linux/modules/mirror_dialog.py +122 -0
  39. moleditpy_linux/modules/molecular_data.py +302 -0
  40. moleditpy_linux/modules/molecule_scene.py +2000 -0
  41. moleditpy_linux/modules/move_group_dialog.py +598 -0
  42. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  43. moleditpy_linux/modules/planarize_dialog.py +221 -0
  44. moleditpy_linux/modules/plugin_interface.py +195 -0
  45. moleditpy_linux/modules/plugin_manager.py +309 -0
  46. moleditpy_linux/modules/plugin_manager_window.py +221 -0
  47. moleditpy_linux/modules/settings_dialog.py +1149 -0
  48. moleditpy_linux/modules/template_preview_item.py +157 -0
  49. moleditpy_linux/modules/template_preview_view.py +74 -0
  50. moleditpy_linux/modules/translation_dialog.py +365 -0
  51. moleditpy_linux/modules/user_template_dialog.py +692 -0
  52. moleditpy_linux/modules/zoomable_view.py +129 -0
  53. moleditpy_linux-2.2.4.dist-info/METADATA +936 -0
  54. moleditpy_linux-2.2.4.dist-info/RECORD +58 -0
  55. moleditpy_linux-2.2.4.dist-info/WHEEL +5 -0
  56. moleditpy_linux-2.2.4.dist-info/entry_points.txt +2 -0
  57. moleditpy_linux-2.2.4.dist-info/licenses/LICENSE +674 -0
  58. moleditpy_linux-2.2.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ """
14
+ plugin_manager.py
15
+ Manages discovery, loading, and execution of external plugins.
16
+ """
17
+
18
+ import os
19
+ import sys
20
+ import shutil
21
+ import importlib.util
22
+ import traceback
23
+ import ast
24
+ from PyQt6.QtGui import QDesktopServices
25
+ from PyQt6.QtCore import QUrl
26
+ from PyQt6.QtWidgets import QMessageBox
27
+
28
+ try:
29
+ from .plugin_interface import PluginContext
30
+ except ImportError:
31
+ # Fallback if running as script
32
+ from modules.plugin_interface import PluginContext
33
+
34
+ class PluginManager:
35
+ def __init__(self, main_window=None):
36
+ self.plugin_dir = os.path.join(os.path.expanduser('~'), '.moleditpy', 'plugins')
37
+ self.plugins = [] # List of dicts
38
+ self.main_window = main_window
39
+
40
+ # Registries for actions
41
+ self.menu_actions = [] # List of (plugin_name, path, callback, text, icon, shortcut)
42
+ self.toolbar_actions = []
43
+ self.drop_handlers = [] # List of (priority, plugin_name, callback)
44
+
45
+ # Extended Registries (Added to prevent lazy initialization "monkey patching")
46
+ self.export_actions = []
47
+ self.optimization_methods = {}
48
+ self.file_openers = {}
49
+ self.analysis_tools = []
50
+ self.save_handlers = {}
51
+ self.load_handlers = {}
52
+ self.custom_3d_styles = {} # style_name -> {'plugin': name, 'callback': func}
53
+
54
+ def get_main_window(self):
55
+ return self.main_window
56
+
57
+ def set_main_window(self, mw):
58
+ self.main_window = mw
59
+
60
+ def ensure_plugin_dir(self):
61
+ """Creates the plugin directory if it doesn't exist."""
62
+ if not os.path.exists(self.plugin_dir):
63
+ try:
64
+ os.makedirs(self.plugin_dir)
65
+ except OSError as e:
66
+ print(f"Error creating plugin directory: {e}")
67
+
68
+ def open_plugin_folder(self):
69
+ """Opens the plugin directory in the OS file explorer."""
70
+ self.ensure_plugin_dir()
71
+ QDesktopServices.openUrl(QUrl.fromLocalFile(self.plugin_dir))
72
+
73
+ def install_plugin(self, file_path):
74
+ """Copies a plugin file to the plugin directory."""
75
+ self.ensure_plugin_dir()
76
+ try:
77
+ filename = os.path.basename(file_path)
78
+ dest_path = os.path.join(self.plugin_dir, filename)
79
+ shutil.copy2(file_path, dest_path)
80
+ # Reload plugins after install
81
+ if self.main_window:
82
+ self.discover_plugins(self.main_window)
83
+ return True, f"Installed {filename}"
84
+ except Exception as e:
85
+ return False, str(e)
86
+
87
+ def discover_plugins(self, parent=None):
88
+ """
89
+ Recursively scans the plugin directory.
90
+ Supports both legacy autorun(parent) and new initialize(context).
91
+ """
92
+ if parent:
93
+ self.main_window = parent
94
+
95
+ self.ensure_plugin_dir()
96
+ self.plugins = []
97
+ self.menu_actions = []
98
+ self.toolbar_actions = []
99
+ self.drop_handlers = []
100
+
101
+ # Clear extended registries
102
+ self.export_actions = []
103
+ self.optimization_methods = {}
104
+ self.file_openers = {}
105
+ self.analysis_tools = []
106
+ self.save_handlers = {}
107
+ self.load_handlers = {}
108
+ self.custom_3d_styles = {}
109
+
110
+ if not os.path.exists(self.plugin_dir):
111
+ return []
112
+
113
+ for root, dirs, files in os.walk(self.plugin_dir):
114
+ # Modify dirs in-place to skip hidden directories and __pycache__
115
+ dirs[:] = [d for d in dirs if not d.startswith('__') and d != '__pycache__']
116
+
117
+ for filename in files:
118
+ if filename.endswith(".py") and not filename.startswith("__"):
119
+ filepath = os.path.join(root, filename)
120
+ rel_folder = os.path.relpath(root, self.plugin_dir)
121
+ if rel_folder == '.':
122
+ rel_folder = ""
123
+
124
+ try:
125
+ module_name = os.path.splitext(os.path.relpath(filepath, self.plugin_dir))[0].replace(os.sep, '.')
126
+
127
+ spec = importlib.util.spec_from_file_location(module_name, filepath)
128
+ if spec and spec.loader:
129
+ module = importlib.util.module_from_spec(spec)
130
+ sys.modules[spec.name] = module
131
+ spec.loader.exec_module(module)
132
+
133
+ # --- Metadata Extraction ---
134
+ plugin_name = getattr(module, 'PLUGIN_NAME', filename[:-3])
135
+ plugin_version = getattr(module, 'PLUGIN_VERSION', getattr(module, '__version__', 'Unknown'))
136
+ plugin_author = getattr(module, 'PLUGIN_AUTHOR', getattr(module, '__author__', 'Unknown'))
137
+ plugin_desc = getattr(module, 'PLUGIN_DESCRIPTION', getattr(module, '__doc__', ''))
138
+
139
+ # Clean up docstring if used as description
140
+ if plugin_desc:
141
+ plugin_desc = plugin_desc.strip().split('\n')[0]
142
+
143
+ # check for interface compliance
144
+ has_run = hasattr(module, 'run') and callable(module.run)
145
+ has_autorun = hasattr(module, 'autorun') and callable(module.autorun)
146
+ has_init = hasattr(module, 'initialize') and callable(module.initialize)
147
+
148
+ status = "Loaded"
149
+
150
+ # Execute loading logic
151
+ if has_init:
152
+ context = PluginContext(self, plugin_name)
153
+ try:
154
+ module.initialize(context)
155
+ except Exception as e:
156
+ status = f"Error (Init): {e}"
157
+ print(f"Plugin {plugin_name} initialize error: {e}")
158
+ traceback.print_exc()
159
+ elif has_autorun:
160
+ try:
161
+ if self.main_window:
162
+ module.autorun(self.main_window)
163
+ else:
164
+ status = "Skipped (No MW)"
165
+ except Exception as e:
166
+ status = f"Error (Autorun): {e}"
167
+ print(f"Plugin {plugin_name} autorun error: {e}")
168
+ traceback.print_exc()
169
+ elif not has_run:
170
+ status = "No Entry Point"
171
+
172
+ self.plugins.append({
173
+ 'name': plugin_name,
174
+ 'version': plugin_version,
175
+ 'author': plugin_author,
176
+ 'description': plugin_desc,
177
+ 'module': module,
178
+ 'rel_folder': rel_folder,
179
+ 'status': status,
180
+ 'filepath': filepath,
181
+ 'has_run': has_run # for menu manual run
182
+ })
183
+
184
+ except Exception as e:
185
+ print(f"Failed to load plugin {filename}: {e}")
186
+ traceback.print_exc()
187
+
188
+ return self.plugins
189
+
190
+ def run_plugin(self, module, main_window):
191
+ """Executes the plugin's run method (Legacy manual trigger)."""
192
+ try:
193
+ module.run(main_window)
194
+ except Exception as e:
195
+ QMessageBox.critical(main_window, "Plugin Error", f"Error running plugin '{getattr(module, 'PLUGIN_NAME', 'Unknown')}':\n{e}")
196
+ traceback.print_exc()
197
+
198
+ # --- Registration Callbacks ---
199
+ def register_menu_action(self, plugin_name, path, callback, text, icon, shortcut):
200
+ self.menu_actions.append({
201
+ 'plugin': plugin_name, 'path': path, 'callback': callback,
202
+ 'text': text, 'icon': icon, 'shortcut': shortcut
203
+ })
204
+
205
+ def register_toolbar_action(self, plugin_name, callback, text, icon, tooltip):
206
+ self.toolbar_actions.append({
207
+ 'plugin': plugin_name, 'callback': callback,
208
+ 'text': text, 'icon': icon, 'tooltip': tooltip
209
+ })
210
+
211
+
212
+
213
+ def register_drop_handler(self, plugin_name, callback, priority):
214
+ self.drop_handlers.append({
215
+ 'priority': priority, 'plugin': plugin_name, 'callback': callback
216
+ })
217
+ # Sort by priority desc
218
+ self.drop_handlers.sort(key=lambda x: x['priority'], reverse=True)
219
+
220
+ def register_export_action(self, plugin_name, label, callback):
221
+ self.export_actions.append({
222
+ 'plugin': plugin_name, 'label': label, 'callback': callback
223
+ })
224
+
225
+ def register_optimization_method(self, plugin_name, method_name, callback):
226
+ # Key by upper-case method name for consistency
227
+ self.optimization_methods[method_name.upper()] = {
228
+ 'plugin': plugin_name, 'callback': callback, 'label': method_name
229
+ }
230
+
231
+ def register_file_opener(self, plugin_name, extension, callback):
232
+ # Normalize extension to lowercase
233
+ ext = extension.lower()
234
+ if not ext.startswith('.'):
235
+ ext = '.' + ext
236
+ self.file_openers[ext] = {
237
+ 'plugin': plugin_name, 'callback': callback
238
+ }
239
+
240
+ # Analysis Tools registration
241
+ def register_analysis_tool(self, plugin_name, label, callback):
242
+ self.analysis_tools.append({'plugin': plugin_name, 'label': label, 'callback': callback})
243
+
244
+ # State Persistence registration
245
+ def register_save_handler(self, plugin_name, callback):
246
+ self.save_handlers[plugin_name] = callback
247
+
248
+ def register_load_handler(self, plugin_name, callback):
249
+ self.load_handlers[plugin_name] = callback
250
+
251
+ def register_3d_style(self, plugin_name, style_name, callback):
252
+ self.custom_3d_styles[style_name] = {
253
+ 'plugin': plugin_name, 'callback': callback
254
+ }
255
+
256
+ def get_plugin_info_safe(self, file_path):
257
+ """Extracts plugin metadata using AST parsing (safe, no execution)."""
258
+ info = {
259
+ 'name': os.path.basename(file_path),
260
+ 'version': 'Unknown',
261
+ 'author': 'Unknown',
262
+ 'description': ''
263
+ }
264
+ try:
265
+ with open(file_path, "r", encoding="utf-8") as f:
266
+ tree = ast.parse(f.read())
267
+
268
+ for node in tree.body:
269
+ if isinstance(node, ast.Assign):
270
+ for target in node.targets:
271
+ if isinstance(target, ast.Name):
272
+ # Helper to extract value
273
+ val = None
274
+ if isinstance(node.value, ast.Constant): # Py3.8+
275
+ val = node.value.value
276
+ elif hasattr(ast, 'Str') and isinstance(node.value, ast.Str): # Py3.7 and below
277
+ val = node.value.s
278
+
279
+ if val is not None:
280
+ if target.id == 'PLUGIN_NAME':
281
+ info['name'] = val
282
+ elif target.id == 'PLUGIN_VERSION':
283
+ info['version'] = val
284
+ elif target.id == 'PLUGIN_AUTHOR':
285
+ info['author'] = val
286
+ elif target.id == 'PLUGIN_DESCRIPTION':
287
+ info['description'] = val
288
+ elif target.id == '__version__' and info['version'] == 'Unknown':
289
+ info['version'] = val
290
+ elif target.id == '__author__' and info['author'] == 'Unknown':
291
+ info['author'] = val
292
+
293
+ # Docstring extraction
294
+ if isinstance(node, ast.Expr):
295
+ val = None
296
+ if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
297
+ val = node.value.value
298
+ elif hasattr(ast, 'Str') and isinstance(node.value, ast.Str):
299
+ val = node.value.s
300
+
301
+ if val and not info['description']:
302
+ info['description'] = val.strip().split('\n')[0]
303
+
304
+ except Exception as e:
305
+ print(f"Error parsing plugin info: {e}")
306
+ return info
307
+
308
+
309
+
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ from PyQt6.QtWidgets import (
16
+ QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QTableWidget,
17
+ QTableWidgetItem, QHeaderView, QLabel, QFileDialog, QMessageBox, QAbstractItemView
18
+ )
19
+ from PyQt6.QtCore import Qt, QMimeData, QUrl
20
+ from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QDesktopServices
21
+
22
+ class PluginManagerWindow(QDialog):
23
+ def __init__(self, plugin_manager, parent=None):
24
+ super().__init__(parent)
25
+ self.plugin_manager = plugin_manager
26
+ self.setWindowTitle("Plugin Manager")
27
+ self.resize(800, 500)
28
+ self.setAcceptDrops(True) # Enable drag & drop for the whole window
29
+
30
+ self.init_ui()
31
+ self.refresh_plugin_list()
32
+
33
+ def init_ui(self):
34
+ layout = QVBoxLayout(self)
35
+
36
+ # Header / Instruction
37
+ lbl_info = QLabel("Drag & Drop .py files here to install plugins.")
38
+ lbl_info.setStyleSheet("color: gray; font-style: italic;")
39
+ layout.addWidget(lbl_info)
40
+
41
+ # Plugin Table
42
+ self.table = QTableWidget()
43
+ self.table.setColumnCount(6)
44
+ self.table.setHorizontalHeaderLabels(["Status", "Name", "Version", "Author", "Location", "Description"])
45
+ self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
46
+ self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Interactive)
47
+ self.table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.Stretch) # Description stretches
48
+ self.table.setColumnWidth(1, 200) # Make Name column wider
49
+ self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
50
+ self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
51
+ self.table.itemSelectionChanged.connect(self.update_button_state)
52
+ self.table.itemDoubleClicked.connect(self.show_plugin_details)
53
+ layout.addWidget(self.table)
54
+
55
+ # Buttons
56
+ btn_layout = QHBoxLayout()
57
+
58
+ btn_reload = QPushButton("Reload Plugins")
59
+ btn_reload.clicked.connect(self.on_reload)
60
+ btn_layout.addWidget(btn_reload)
61
+
62
+ btn_folder = QPushButton("Open Plugin Folder")
63
+ btn_folder.clicked.connect(self.plugin_manager.open_plugin_folder)
64
+ btn_layout.addWidget(btn_folder)
65
+
66
+ self.btn_remove = QPushButton("Remove Plugin")
67
+ self.btn_remove.clicked.connect(self.on_remove_plugin)
68
+ self.btn_remove.setEnabled(False)
69
+ btn_layout.addWidget(self.btn_remove)
70
+
71
+ btn_explore = QPushButton("Explore Plugins Online")
72
+ btn_explore.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://hiroyokoyama.github.io/moleditpy-plugins/explorer/")))
73
+ btn_layout.addWidget(btn_explore)
74
+
75
+ btn_close = QPushButton("Close")
76
+ btn_close.clicked.connect(self.close)
77
+ btn_layout.addStretch()
78
+ btn_layout.addWidget(btn_close)
79
+
80
+ layout.addLayout(btn_layout)
81
+
82
+ def refresh_plugin_list(self):
83
+ self.table.setRowCount(0)
84
+ plugins = self.plugin_manager.plugins
85
+
86
+ self.table.setRowCount(len(plugins))
87
+ for row, p in enumerate(plugins):
88
+ status_item = QTableWidgetItem(str(p.get('status', 'Unknown')))
89
+ status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
90
+ self.table.setItem(row, 0, status_item)
91
+ self.table.setItem(row, 1, QTableWidgetItem(str(p.get('name', 'Unknown'))))
92
+ self.table.setItem(row, 2, QTableWidgetItem(str(p.get('version', ''))))
93
+ self.table.setItem(row, 3, QTableWidgetItem(str(p.get('author', ''))))
94
+
95
+ # Location (Relative Path)
96
+ full_path = p.get('filepath', '')
97
+ rel_path = ""
98
+ if full_path:
99
+ try:
100
+ rel_path = os.path.relpath(full_path, self.plugin_manager.plugin_dir)
101
+ except Exception:
102
+ rel_path = os.path.basename(full_path)
103
+ self.table.setItem(row, 4, QTableWidgetItem(str(rel_path)))
104
+
105
+ self.table.setItem(row, 5, QTableWidgetItem(str(p.get('description', ''))))
106
+
107
+ # Simple color coding for status
108
+ status = str(p.get('status', ''))
109
+ color = None
110
+ if status.startswith("Error"):
111
+ color = Qt.GlobalColor.red
112
+ elif status == "Loaded":
113
+ color = Qt.GlobalColor.darkGreen
114
+ elif status == "No Entry Point":
115
+ color = Qt.GlobalColor.gray
116
+
117
+ if color:
118
+ self.table.item(row, 0).setForeground(color)
119
+
120
+ def update_button_state(self):
121
+ has_selection = (self.table.currentRow() >= 0)
122
+ if hasattr(self, 'btn_remove'):
123
+ self.btn_remove.setEnabled(has_selection)
124
+
125
+ def on_reload(self, silent=False):
126
+ # Trigger reload in main manager
127
+ if self.plugin_manager.main_window:
128
+ self.plugin_manager.discover_plugins(self.plugin_manager.main_window)
129
+ self.refresh_plugin_list()
130
+ # Also update main window menu if possible, but that might require a callback or signal
131
+ # For now we assume discover_plugins re-runs autoruns which might duplicate stuff if not careful?
132
+ # Actually discover_plugins clears lists, so re-running is safe logic-wise,
133
+ # but main_window need to rebuild its menu.
134
+ # We will handle UI rebuild in the main window code by observing or callback.
135
+
136
+ # For immediate feedback:
137
+ if not silent:
138
+ QMessageBox.information(self, "Reloaded", "Plugins have been reloaded.")
139
+ else:
140
+ self.plugin_manager.discover_plugins()
141
+ self.refresh_plugin_list()
142
+
143
+ def on_remove_plugin(self):
144
+ row = self.table.currentRow()
145
+ if row < 0:
146
+ QMessageBox.warning(self, "Warning", "Please select a plugin to remove.")
147
+ return
148
+
149
+ # Assuming table row index matches plugins list index (confirmed in refresh_plugin_list)
150
+ if row < len(self.plugin_manager.plugins):
151
+ plugin = self.plugin_manager.plugins[row]
152
+ filepath = plugin.get('filepath')
153
+
154
+ if filepath and os.path.exists(filepath):
155
+ reply = QMessageBox.question(self, "Remove Plugin",
156
+ f"Are you sure you want to remove '{plugin.get('name', 'Unknown')}'?\n\nFile: {filepath}\nThis cannot be undone.",
157
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
158
+ if reply == QMessageBox.StandardButton.Yes:
159
+ try:
160
+ os.remove(filepath)
161
+ self.on_reload(silent=True) # Reload list and plugins
162
+ QMessageBox.information(self, "Success", f"Removed '{plugin.get('name', 'Unknown')}'.")
163
+ except Exception as e:
164
+ QMessageBox.critical(self, "Error", f"Failed to delete file: {e}")
165
+ else:
166
+ QMessageBox.warning(self, "Error", f"Plugin file not found:\n{filepath}")
167
+
168
+ def show_plugin_details(self, item):
169
+ row = item.row()
170
+ if row < len(self.plugin_manager.plugins):
171
+ p = self.plugin_manager.plugins[row]
172
+ msg = f"Name: {p.get('name', 'Unknown')}\n" \
173
+ f"Version: {p.get('version', 'Unknown')}\n" \
174
+ f"Author: {p.get('author', 'Unknown')}\n" \
175
+ f"Status: {p.get('status', 'Unknown')}\n" \
176
+ f"Location: {p.get('filepath', 'Unknown')}\n\n" \
177
+ f"Description:\n{p.get('description', 'No description available.')}"
178
+ QMessageBox.information(self, "Plugin Details", msg)
179
+
180
+ # --- Drag & Drop Support ---
181
+ def dragEnterEvent(self, event: QDragEnterEvent):
182
+ if event.mimeData().hasUrls():
183
+ event.accept()
184
+ else:
185
+ event.ignore()
186
+
187
+ def dropEvent(self, event: QDropEvent):
188
+ files_installed = []
189
+ errors = []
190
+ for url in event.mimeData().urls():
191
+ file_path = url.toLocalFile()
192
+ if os.path.isfile(file_path) and file_path.endswith('.py'):
193
+ # Extract info and confirm
194
+ info = self.plugin_manager.get_plugin_info_safe(file_path)
195
+ msg = (f"Do you want to install this plugin?\n\n"
196
+ f"Name: {info['name']}\n"
197
+ f"Author: {info['author']}\n"
198
+ f"Version: {info['version']}\n"
199
+ f"Description: {info['description']}\n\n"
200
+ f"File: {os.path.basename(file_path)}")
201
+
202
+ reply = QMessageBox.question(self, "Install Plugin?", msg,
203
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
204
+
205
+ if reply == QMessageBox.StandardButton.Yes:
206
+ success, msg = self.plugin_manager.install_plugin(file_path)
207
+ if success:
208
+ files_installed.append(msg)
209
+ else:
210
+ errors.append(msg)
211
+
212
+ if files_installed or errors:
213
+ self.refresh_plugin_list()
214
+ summary = ""
215
+ if files_installed:
216
+ summary += "Installed:\n" + "\n".join(files_installed) + "\n\n"
217
+ if errors:
218
+ summary += "Errors:\n" + "\n".join(errors)
219
+
220
+ QMessageBox.information(self, "Plugin Installation", summary)
221
+