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.
- moleditpy_linux/__init__.py +17 -0
- moleditpy_linux/__main__.py +29 -0
- moleditpy_linux/main.py +37 -0
- moleditpy_linux/modules/__init__.py +41 -0
- moleditpy_linux/modules/about_dialog.py +104 -0
- moleditpy_linux/modules/align_plane_dialog.py +293 -0
- moleditpy_linux/modules/alignment_dialog.py +273 -0
- moleditpy_linux/modules/analysis_window.py +209 -0
- moleditpy_linux/modules/angle_dialog.py +440 -0
- moleditpy_linux/modules/assets/icon.icns +0 -0
- moleditpy_linux/modules/assets/icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.png +0 -0
- moleditpy_linux/modules/atom_item.py +348 -0
- moleditpy_linux/modules/bond_item.py +406 -0
- moleditpy_linux/modules/bond_length_dialog.py +380 -0
- moleditpy_linux/modules/calculation_worker.py +766 -0
- moleditpy_linux/modules/color_settings_dialog.py +321 -0
- moleditpy_linux/modules/constants.py +88 -0
- moleditpy_linux/modules/constrained_optimization_dialog.py +679 -0
- moleditpy_linux/modules/custom_interactor_style.py +749 -0
- moleditpy_linux/modules/custom_qt_interactor.py +59 -0
- moleditpy_linux/modules/dialog3_d_picking_mixin.py +108 -0
- moleditpy_linux/modules/dihedral_dialog.py +443 -0
- moleditpy_linux/modules/main_window.py +842 -0
- moleditpy_linux/modules/main_window_app_state.py +780 -0
- moleditpy_linux/modules/main_window_compute.py +1242 -0
- moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
- moleditpy_linux/modules/main_window_edit_3d.py +536 -0
- moleditpy_linux/modules/main_window_edit_actions.py +1455 -0
- moleditpy_linux/modules/main_window_export.py +806 -0
- moleditpy_linux/modules/main_window_main_init.py +2006 -0
- moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
- moleditpy_linux/modules/main_window_project_io.py +434 -0
- moleditpy_linux/modules/main_window_string_importers.py +275 -0
- moleditpy_linux/modules/main_window_ui_manager.py +606 -0
- moleditpy_linux/modules/main_window_view_3d.py +1531 -0
- moleditpy_linux/modules/main_window_view_loaders.py +355 -0
- moleditpy_linux/modules/mirror_dialog.py +122 -0
- moleditpy_linux/modules/molecular_data.py +302 -0
- moleditpy_linux/modules/molecule_scene.py +2000 -0
- moleditpy_linux/modules/move_group_dialog.py +598 -0
- moleditpy_linux/modules/periodic_table_dialog.py +84 -0
- moleditpy_linux/modules/planarize_dialog.py +221 -0
- moleditpy_linux/modules/plugin_interface.py +195 -0
- moleditpy_linux/modules/plugin_manager.py +309 -0
- moleditpy_linux/modules/plugin_manager_window.py +221 -0
- moleditpy_linux/modules/settings_dialog.py +1149 -0
- moleditpy_linux/modules/template_preview_item.py +157 -0
- moleditpy_linux/modules/template_preview_view.py +74 -0
- moleditpy_linux/modules/translation_dialog.py +365 -0
- moleditpy_linux/modules/user_template_dialog.py +692 -0
- moleditpy_linux/modules/zoomable_view.py +129 -0
- moleditpy_linux-2.2.4.dist-info/METADATA +936 -0
- moleditpy_linux-2.2.4.dist-info/RECORD +58 -0
- moleditpy_linux-2.2.4.dist-info/WHEEL +5 -0
- moleditpy_linux-2.2.4.dist-info/entry_points.txt +2 -0
- moleditpy_linux-2.2.4.dist-info/licenses/LICENSE +674 -0
- 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
|
+
|