MoleditPy 2.2.5__py3-none-any.whl → 2.3.0__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/modules/align_plane_dialog.py +0 -1
- moleditpy/modules/alignment_dialog.py +0 -1
- moleditpy/modules/angle_dialog.py +1 -1
- moleditpy/modules/bond_length_dialog.py +1 -1
- moleditpy/modules/constants.py +1 -1
- moleditpy/modules/constrained_optimization_dialog.py +0 -1
- moleditpy/modules/custom_interactor_style.py +56 -56
- moleditpy/modules/custom_qt_interactor.py +43 -0
- moleditpy/modules/dialog3_d_picking_mixin.py +42 -9
- moleditpy/modules/dihedral_dialog.py +1 -1
- moleditpy/modules/main_window_dialog_manager.py +9 -9
- moleditpy/modules/main_window_edit_actions.py +4 -1
- moleditpy/modules/main_window_main_init.py +51 -27
- moleditpy/modules/main_window_view_3d.py +9 -1
- moleditpy/modules/move_group_dialog.py +7 -5
- moleditpy/modules/planarize_dialog.py +0 -1
- moleditpy/modules/plugin_manager.py +234 -95
- moleditpy/modules/plugin_manager_window.py +67 -14
- moleditpy/modules/translation_dialog.py +0 -1
- {moleditpy-2.2.5.dist-info → moleditpy-2.3.0.dist-info}/METADATA +1 -1
- {moleditpy-2.2.5.dist-info → moleditpy-2.3.0.dist-info}/RECORD +25 -25
- {moleditpy-2.2.5.dist-info → moleditpy-2.3.0.dist-info}/WHEEL +0 -0
- {moleditpy-2.2.5.dist-info → moleditpy-2.3.0.dist-info}/entry_points.txt +0 -0
- {moleditpy-2.2.5.dist-info → moleditpy-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-2.2.5.dist-info → moleditpy-2.3.0.dist-info}/top_level.txt +0 -0
|
@@ -42,7 +42,6 @@ class MoveGroupDialog(Dialog3DPickingMixin, QDialog):
|
|
|
42
42
|
def init_ui(self):
|
|
43
43
|
self.setWindowTitle("Move Group")
|
|
44
44
|
self.setModal(False)
|
|
45
|
-
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
|
|
46
45
|
self.resize(300,400) # ウィンドウサイズを設定
|
|
47
46
|
layout = QVBoxLayout(self)
|
|
48
47
|
|
|
@@ -195,9 +194,12 @@ class MoveGroupDialog(Dialog3DPickingMixin, QDialog):
|
|
|
195
194
|
if 0 <= closest_atom_idx < self.mol.GetNumAtoms():
|
|
196
195
|
atom = self.mol.GetAtomWithIdx(int(closest_atom_idx))
|
|
197
196
|
if atom:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
try:
|
|
198
|
+
atomic_num = atom.GetAtomicNum()
|
|
199
|
+
vdw_radius = pt.GetRvdw(atomic_num)
|
|
200
|
+
if vdw_radius < 0.1: vdw_radius = 1.5
|
|
201
|
+
except Exception:
|
|
202
|
+
vdw_radius = 1.5
|
|
201
203
|
click_threshold = vdw_radius * 1.5
|
|
202
204
|
|
|
203
205
|
if distances[closest_atom_idx] < click_threshold:
|
|
@@ -349,7 +351,7 @@ class MoveGroupDialog(Dialog3DPickingMixin, QDialog):
|
|
|
349
351
|
return False
|
|
350
352
|
|
|
351
353
|
# その他のイベントは親クラスに渡す
|
|
352
|
-
return
|
|
354
|
+
return super().eventFilter(obj, event)
|
|
353
355
|
|
|
354
356
|
def on_atom_picked(self, atom_idx):
|
|
355
357
|
"""原子がピックされたときに、その原子が属する連結成分全体を選択(複数グループ対応)"""
|
|
@@ -46,7 +46,6 @@ class PlanarizeDialog(Dialog3DPickingMixin, QDialog):
|
|
|
46
46
|
def init_ui(self):
|
|
47
47
|
self.setWindowTitle("Planarize")
|
|
48
48
|
self.setModal(False)
|
|
49
|
-
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
|
|
50
49
|
layout = QVBoxLayout(self)
|
|
51
50
|
|
|
52
51
|
instruction_label = QLabel("Click atoms in the 3D view to select them for planarization (minimum 3 required).")
|
|
@@ -18,6 +18,7 @@ Manages discovery, loading, and execution of external plugins.
|
|
|
18
18
|
import os
|
|
19
19
|
import sys
|
|
20
20
|
import shutil
|
|
21
|
+
import zipfile
|
|
21
22
|
import importlib.util
|
|
22
23
|
import traceback
|
|
23
24
|
import ast
|
|
@@ -71,34 +72,110 @@ class PluginManager:
|
|
|
71
72
|
QDesktopServices.openUrl(QUrl.fromLocalFile(self.plugin_dir))
|
|
72
73
|
|
|
73
74
|
def install_plugin(self, file_path):
|
|
74
|
-
"""Copies a plugin file to the plugin directory."""
|
|
75
|
+
"""Copies a plugin file to the plugin directory. Supports .py and .zip."""
|
|
75
76
|
self.ensure_plugin_dir()
|
|
76
77
|
try:
|
|
78
|
+
# Handle trailing slash and normalize path
|
|
79
|
+
file_path = os.path.normpath(file_path)
|
|
77
80
|
filename = os.path.basename(file_path)
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
|
|
82
|
+
if os.path.isdir(file_path):
|
|
83
|
+
# Copy entire directory
|
|
84
|
+
dest_path = os.path.join(self.plugin_dir, filename)
|
|
85
|
+
if os.path.exists(dest_path):
|
|
86
|
+
# Option 1: Overwrite (remove then copy) - safer for clean install
|
|
87
|
+
if os.path.isdir(dest_path):
|
|
88
|
+
shutil.rmtree(dest_path)
|
|
89
|
+
else:
|
|
90
|
+
os.remove(dest_path)
|
|
91
|
+
|
|
92
|
+
# Copy directory, ignoring cache files
|
|
93
|
+
shutil.copytree(file_path, dest_path, ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git'))
|
|
94
|
+
msg = f"Installed package {filename}"
|
|
95
|
+
elif filename.lower().endswith('.zip'):
|
|
96
|
+
# Extract ZIP contents
|
|
97
|
+
with zipfile.ZipFile(file_path, 'r') as zf:
|
|
98
|
+
# Smart Extraction: Check if ZIP has a single top-level folder
|
|
99
|
+
# Fix for paths with backslashes on Windows if zip was created on Windows
|
|
100
|
+
roots = set()
|
|
101
|
+
for name in zf.namelist():
|
|
102
|
+
# Normalize path separators to forward slash for consistent check
|
|
103
|
+
name = name.replace('\\', '/')
|
|
104
|
+
parts = name.split('/')
|
|
105
|
+
if parts[0]:
|
|
106
|
+
roots.add(parts[0])
|
|
107
|
+
|
|
108
|
+
is_nested = (len(roots) == 1)
|
|
109
|
+
|
|
110
|
+
if is_nested:
|
|
111
|
+
# Case A: ZIP contains a single folder (e.g. MyPlugin/init.py)
|
|
112
|
+
top_folder = list(roots)[0]
|
|
113
|
+
|
|
114
|
+
# Guard: If the single item is __init__.py, we MUST create a wrapper folder
|
|
115
|
+
# otherwise we pollute the plugin_dir root.
|
|
116
|
+
if top_folder == "__init__.py":
|
|
117
|
+
is_nested = False
|
|
118
|
+
|
|
119
|
+
if is_nested:
|
|
120
|
+
# Case A (Confirmed): Extract directly
|
|
121
|
+
dest_path = os.path.join(self.plugin_dir, top_folder)
|
|
122
|
+
|
|
123
|
+
# Clean Install: Remove existing folder to prevent stale files
|
|
124
|
+
if os.path.exists(dest_path):
|
|
125
|
+
if os.path.isdir(dest_path):
|
|
126
|
+
shutil.rmtree(dest_path)
|
|
127
|
+
else:
|
|
128
|
+
os.remove(dest_path)
|
|
129
|
+
|
|
130
|
+
zf.extractall(self.plugin_dir)
|
|
131
|
+
msg = f"Installed package {top_folder} (from ZIP)"
|
|
132
|
+
else:
|
|
133
|
+
# Case B: ZIP is flat (e.g. file1.py, file2.py or just __init__.py)
|
|
134
|
+
# Extract into a new folder named after the ZIP file
|
|
135
|
+
folder_name = os.path.splitext(filename)[0]
|
|
136
|
+
dest_path = os.path.join(self.plugin_dir, folder_name)
|
|
137
|
+
|
|
138
|
+
if os.path.exists(dest_path):
|
|
139
|
+
if os.path.isdir(dest_path):
|
|
140
|
+
shutil.rmtree(dest_path)
|
|
141
|
+
else:
|
|
142
|
+
os.remove(dest_path)
|
|
143
|
+
|
|
144
|
+
os.makedirs(dest_path)
|
|
145
|
+
zf.extractall(dest_path)
|
|
146
|
+
msg = f"Installed package {folder_name} (from Flat ZIP)"
|
|
147
|
+
else:
|
|
148
|
+
# Standard file copy
|
|
149
|
+
dest_path = os.path.join(self.plugin_dir, filename)
|
|
150
|
+
if os.path.exists(dest_path):
|
|
151
|
+
if os.path.isdir(dest_path):
|
|
152
|
+
import shutil
|
|
153
|
+
shutil.rmtree(dest_path)
|
|
154
|
+
shutil.copy2(file_path, dest_path)
|
|
155
|
+
msg = f"Installed {filename}"
|
|
156
|
+
|
|
80
157
|
# Reload plugins after install
|
|
81
158
|
if self.main_window:
|
|
82
159
|
self.discover_plugins(self.main_window)
|
|
83
|
-
return True,
|
|
160
|
+
return True, msg
|
|
84
161
|
except Exception as e:
|
|
85
162
|
return False, str(e)
|
|
86
163
|
|
|
87
164
|
def discover_plugins(self, parent=None):
|
|
88
165
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
166
|
+
Hybrid discovery:
|
|
167
|
+
- Folders with '__init__.py' -> Treated as single package plugin.
|
|
168
|
+
- Folders without '__init__.py' -> Treated as category folders (scan for .py inside).
|
|
91
169
|
"""
|
|
92
170
|
if parent:
|
|
93
171
|
self.main_window = parent
|
|
94
172
|
|
|
95
173
|
self.ensure_plugin_dir()
|
|
96
174
|
self.plugins = []
|
|
175
|
+
# Clear registries
|
|
97
176
|
self.menu_actions = []
|
|
98
177
|
self.toolbar_actions = []
|
|
99
178
|
self.drop_handlers = []
|
|
100
|
-
|
|
101
|
-
# Clear extended registries
|
|
102
179
|
self.export_actions = []
|
|
103
180
|
self.optimization_methods = {}
|
|
104
181
|
self.file_openers = {}
|
|
@@ -111,81 +188,122 @@ class PluginManager:
|
|
|
111
188
|
return []
|
|
112
189
|
|
|
113
190
|
for root, dirs, files in os.walk(self.plugin_dir):
|
|
114
|
-
#
|
|
191
|
+
# Exclude hidden directories
|
|
115
192
|
dirs[:] = [d for d in dirs if not d.startswith('__') and d != '__pycache__']
|
|
116
193
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
194
|
+
# [Check] Is current dir a package (plugin body)?
|
|
195
|
+
if "__init__.py" in files:
|
|
196
|
+
# === Case 1: Package Plugin (Folder is the plugin) ===
|
|
197
|
+
|
|
198
|
+
# Stop recursion into this folder
|
|
199
|
+
dirs[:] = []
|
|
200
|
+
|
|
201
|
+
entry_point = os.path.join(root, "__init__.py")
|
|
202
|
+
# Category is relative path to parent folder
|
|
203
|
+
rel_path = os.path.relpath(os.path.dirname(root), self.plugin_dir)
|
|
204
|
+
category = rel_path if rel_path != "." else ""
|
|
205
|
+
|
|
206
|
+
# Module name is the folder name
|
|
207
|
+
module_name = os.path.basename(root)
|
|
208
|
+
|
|
209
|
+
self._load_single_plugin(entry_point, module_name, category)
|
|
210
|
+
|
|
211
|
+
else:
|
|
212
|
+
# === Case 2: Category Folder (Load individual .py files) ===
|
|
213
|
+
|
|
214
|
+
# Category is relative path to current folder
|
|
215
|
+
rel_path = os.path.relpath(root, self.plugin_dir)
|
|
216
|
+
category = rel_path if rel_path != "." else ""
|
|
217
|
+
|
|
218
|
+
for filename in files:
|
|
219
|
+
if filename.endswith(".py") and not filename.startswith("__"):
|
|
220
|
+
entry_point = os.path.join(root, filename)
|
|
221
|
+
module_name = os.path.splitext(filename)[0]
|
|
123
222
|
|
|
223
|
+
self._load_single_plugin(entry_point, module_name, category)
|
|
224
|
+
|
|
225
|
+
return self.plugins
|
|
226
|
+
|
|
227
|
+
def _load_single_plugin(self, filepath, module_name, category):
|
|
228
|
+
"""Common loading logic for both single-file and package plugins."""
|
|
229
|
+
try:
|
|
230
|
+
# Ensure unique module name by including category path
|
|
231
|
+
# e.g. Analysis.Docking
|
|
232
|
+
unique_module_name = f"{category.replace(os.sep, '.')}.{module_name}" if category else module_name
|
|
233
|
+
unique_module_name = unique_module_name.strip(".")
|
|
234
|
+
|
|
235
|
+
spec = importlib.util.spec_from_file_location(unique_module_name, filepath)
|
|
236
|
+
if spec and spec.loader:
|
|
237
|
+
module = importlib.util.module_from_spec(spec)
|
|
238
|
+
sys.modules[spec.name] = module
|
|
239
|
+
|
|
240
|
+
# Inject category info
|
|
241
|
+
module.PLUGIN_CATEGORY = category
|
|
242
|
+
|
|
243
|
+
spec.loader.exec_module(module)
|
|
244
|
+
|
|
245
|
+
# --- Metadata Extraction ---
|
|
246
|
+
# Metadata
|
|
247
|
+
# Priority: PLUGIN_XXX > __xxx__ > Fallback
|
|
248
|
+
plugin_name = getattr(module, 'PLUGIN_NAME', module_name)
|
|
249
|
+
plugin_version = getattr(module, 'PLUGIN_VERSION', getattr(module, '__version__', 'Unknown'))
|
|
250
|
+
plugin_author = getattr(module, 'PLUGIN_AUTHOR', getattr(module, '__author__', 'Unknown'))
|
|
251
|
+
plugin_desc = getattr(module, 'PLUGIN_DESCRIPTION', getattr(module, '__doc__', ''))
|
|
252
|
+
plugin_category = getattr(module, 'PLUGIN_CATEGORY', category)
|
|
253
|
+
|
|
254
|
+
# Additional cleanup for docstring (strip whitespace)
|
|
255
|
+
if plugin_desc is None: plugin_desc = ""
|
|
256
|
+
plugin_desc = str(plugin_desc).strip()
|
|
257
|
+
|
|
258
|
+
# Handle version tuple
|
|
259
|
+
if isinstance(plugin_version, tuple):
|
|
260
|
+
plugin_version = ".".join(map(str, plugin_version))
|
|
261
|
+
|
|
262
|
+
# Interface compliance
|
|
263
|
+
has_run = hasattr(module, 'run') and callable(module.run)
|
|
264
|
+
has_autorun = hasattr(module, 'autorun') and callable(module.autorun)
|
|
265
|
+
has_init = hasattr(module, 'initialize') and callable(module.initialize)
|
|
266
|
+
|
|
267
|
+
status = "Loaded"
|
|
268
|
+
|
|
269
|
+
# Execute initialization
|
|
270
|
+
if has_init:
|
|
271
|
+
context = PluginContext(self, plugin_name)
|
|
272
|
+
# Pass category to context if needed, currently not storing it in context directly but could be useful
|
|
124
273
|
try:
|
|
125
|
-
|
|
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
|
-
|
|
274
|
+
module.initialize(context)
|
|
184
275
|
except Exception as e:
|
|
185
|
-
|
|
276
|
+
status = f"Error (Init): {e}"
|
|
277
|
+
print(f"Plugin {plugin_name} initialize error: {e}")
|
|
186
278
|
traceback.print_exc()
|
|
187
|
-
|
|
188
|
-
|
|
279
|
+
elif has_autorun:
|
|
280
|
+
try:
|
|
281
|
+
if self.main_window:
|
|
282
|
+
module.autorun(self.main_window)
|
|
283
|
+
else:
|
|
284
|
+
status = "Skipped (No MW)"
|
|
285
|
+
except Exception as e:
|
|
286
|
+
status = f"Error (Autorun): {e}"
|
|
287
|
+
print(f"Plugin {plugin_name} autorun error: {e}")
|
|
288
|
+
traceback.print_exc()
|
|
289
|
+
elif not has_run:
|
|
290
|
+
status = "No Entry Point"
|
|
291
|
+
|
|
292
|
+
self.plugins.append({
|
|
293
|
+
'name': plugin_name,
|
|
294
|
+
'version': plugin_version,
|
|
295
|
+
'author': plugin_author,
|
|
296
|
+
'description': plugin_desc,
|
|
297
|
+
'module': module,
|
|
298
|
+
'category': plugin_category, # Store category
|
|
299
|
+
'status': status,
|
|
300
|
+
'filepath': filepath,
|
|
301
|
+
'has_run': has_run
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
print(f"Failed to load plugin {module_name}: {e}")
|
|
306
|
+
traceback.print_exc()
|
|
189
307
|
|
|
190
308
|
def run_plugin(self, module, main_window):
|
|
191
309
|
"""Executes the plugin's run method (Legacy manual trigger)."""
|
|
@@ -266,29 +384,50 @@ class PluginManager:
|
|
|
266
384
|
tree = ast.parse(f.read())
|
|
267
385
|
|
|
268
386
|
for node in tree.body:
|
|
387
|
+
targets = []
|
|
269
388
|
if isinstance(node, ast.Assign):
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
389
|
+
targets = node.targets
|
|
390
|
+
elif isinstance(node, ast.AnnAssign):
|
|
391
|
+
targets = [node.target]
|
|
392
|
+
|
|
393
|
+
for target in targets:
|
|
394
|
+
if isinstance(target, ast.Name):
|
|
395
|
+
# Helper to extract value
|
|
396
|
+
val = None
|
|
397
|
+
if node.value: # AnnAssign might presumably not have value? (though usually does for module globals)
|
|
274
398
|
if isinstance(node.value, ast.Constant): # Py3.8+
|
|
275
399
|
val = node.value.value
|
|
276
400
|
elif hasattr(ast, 'Str') and isinstance(node.value, ast.Str): # Py3.7 and below
|
|
277
401
|
val = node.value.s
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
402
|
+
elif isinstance(node.value, ast.Tuple):
|
|
403
|
+
# Handle version tuples e.g. (1, 0, 0)
|
|
404
|
+
try:
|
|
405
|
+
# Extract simple constants from tuple
|
|
406
|
+
elts = []
|
|
407
|
+
for elt in node.value.elts:
|
|
408
|
+
if isinstance(elt, ast.Constant):
|
|
409
|
+
elts.append(elt.value)
|
|
410
|
+
elif hasattr(ast, 'Num') and isinstance(elt, ast.Num):
|
|
411
|
+
elts.append(elt.n)
|
|
412
|
+
val = ".".join(map(str, elts))
|
|
413
|
+
except:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
if val is not None:
|
|
417
|
+
if target.id == 'PLUGIN_NAME':
|
|
418
|
+
info['name'] = val
|
|
419
|
+
elif target.id == 'PLUGIN_VERSION':
|
|
420
|
+
info['version'] = val
|
|
421
|
+
elif target.id == 'PLUGIN_AUTHOR':
|
|
422
|
+
info['author'] = val
|
|
423
|
+
elif target.id == 'PLUGIN_DESCRIPTION':
|
|
424
|
+
info['description'] = val
|
|
425
|
+
elif target.id == 'PLUGIN_CATEGORY':
|
|
426
|
+
info['category'] = val
|
|
427
|
+
elif target.id == '__version__' and info['version'] == 'Unknown':
|
|
428
|
+
info['version'] = val
|
|
429
|
+
elif target.id == '__author__' and info['author'] == 'Unknown':
|
|
430
|
+
info['author'] = val
|
|
292
431
|
|
|
293
432
|
# Docstring extraction
|
|
294
433
|
if isinstance(node, ast.Expr):
|
|
@@ -18,6 +18,7 @@ from PyQt6.QtWidgets import (
|
|
|
18
18
|
)
|
|
19
19
|
from PyQt6.QtCore import Qt, QMimeData, QUrl
|
|
20
20
|
from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QDesktopServices
|
|
21
|
+
import shutil
|
|
21
22
|
|
|
22
23
|
class PluginManagerWindow(QDialog):
|
|
23
24
|
def __init__(self, plugin_manager, parent=None):
|
|
@@ -33,8 +34,7 @@ class PluginManagerWindow(QDialog):
|
|
|
33
34
|
def init_ui(self):
|
|
34
35
|
layout = QVBoxLayout(self)
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
lbl_info = QLabel("Drag & Drop .py files here to install plugins.")
|
|
37
|
+
lbl_info = QLabel("Drag & Drop .py or .zip files here to install plugins.")
|
|
38
38
|
lbl_info.setStyleSheet("color: gray; font-style: italic;")
|
|
39
39
|
layout.addWidget(lbl_info)
|
|
40
40
|
|
|
@@ -152,16 +152,31 @@ class PluginManagerWindow(QDialog):
|
|
|
152
152
|
filepath = plugin.get('filepath')
|
|
153
153
|
|
|
154
154
|
if filepath and os.path.exists(filepath):
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
155
|
+
# Check if it is a package plugin (based on __init__.py)
|
|
156
|
+
is_package = os.path.basename(filepath) == "__init__.py"
|
|
157
|
+
target_path = os.path.dirname(filepath) if is_package else filepath
|
|
158
|
+
|
|
159
|
+
msg = f"Are you sure you want to remove '{plugin.get('name', 'Unknown')}'?"
|
|
160
|
+
if is_package:
|
|
161
|
+
msg += f"\n\nThis will delete the entire folder:\n{target_path}"
|
|
162
|
+
else:
|
|
163
|
+
msg += f"\n\nFile: {filepath}"
|
|
164
|
+
|
|
165
|
+
msg += "\nThis cannot be undone."
|
|
166
|
+
|
|
167
|
+
reply = QMessageBox.question(self, "Remove Plugin", msg,
|
|
168
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
169
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
170
|
+
try:
|
|
171
|
+
if is_package:
|
|
172
|
+
shutil.rmtree(target_path)
|
|
173
|
+
else:
|
|
174
|
+
os.remove(target_path)
|
|
175
|
+
|
|
176
|
+
self.on_reload(silent=True) # Reload list and plugins
|
|
177
|
+
QMessageBox.information(self, "Success", f"Removed '{plugin.get('name', 'Unknown')}'.")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
QMessageBox.critical(self, "Error", f"Failed to delete plugin: {e}")
|
|
165
180
|
else:
|
|
166
181
|
QMessageBox.warning(self, "Error", f"Plugin file not found:\n{filepath}")
|
|
167
182
|
|
|
@@ -189,9 +204,47 @@ class PluginManagerWindow(QDialog):
|
|
|
189
204
|
errors = []
|
|
190
205
|
for url in event.mimeData().urls():
|
|
191
206
|
file_path = url.toLocalFile()
|
|
192
|
-
|
|
207
|
+
|
|
208
|
+
is_valid = False
|
|
209
|
+
is_zip = False
|
|
210
|
+
is_folder = False
|
|
211
|
+
|
|
212
|
+
if os.path.isfile(file_path):
|
|
213
|
+
# Special handling: If user drops __init__.py, assume they want to install the package (folder)
|
|
214
|
+
if os.path.basename(file_path) == "__init__.py":
|
|
215
|
+
file_path = os.path.dirname(file_path)
|
|
216
|
+
is_valid = True
|
|
217
|
+
is_folder = True
|
|
218
|
+
elif file_path.endswith('.py'):
|
|
219
|
+
is_valid = True
|
|
220
|
+
elif file_path.endswith('.zip'):
|
|
221
|
+
is_valid = True
|
|
222
|
+
is_zip = True
|
|
223
|
+
|
|
224
|
+
if os.path.isdir(file_path):
|
|
225
|
+
# Check for __init__.py to confirm it's a plugin package?
|
|
226
|
+
# Or just assume any folder is fair game (could be category folder too?)
|
|
227
|
+
# We'll allow any folder and let manager handle it.
|
|
228
|
+
is_valid = True
|
|
229
|
+
is_folder = True
|
|
230
|
+
|
|
231
|
+
if is_valid:
|
|
193
232
|
# Extract info and confirm
|
|
194
|
-
info =
|
|
233
|
+
info = {'name': os.path.basename(file_path), 'version': 'Unknown', 'author': 'Unknown', 'description': ''}
|
|
234
|
+
|
|
235
|
+
if is_folder:
|
|
236
|
+
info['description'] = "Folder Plugin / Category"
|
|
237
|
+
# Try to parse __init__.py if it exists
|
|
238
|
+
init_path = os.path.join(file_path, "__init__.py")
|
|
239
|
+
if os.path.exists(init_path):
|
|
240
|
+
info = self.plugin_manager.get_plugin_info_safe(init_path)
|
|
241
|
+
info['description'] += f" (Package: {info['name']})"
|
|
242
|
+
|
|
243
|
+
elif is_zip:
|
|
244
|
+
info['description'] = "ZIP Package Plugin"
|
|
245
|
+
elif file_path.endswith('.py'):
|
|
246
|
+
info = self.plugin_manager.get_plugin_info_safe(file_path)
|
|
247
|
+
|
|
195
248
|
msg = (f"Do you want to install this plugin?\n\n"
|
|
196
249
|
f"Name: {info['name']}\n"
|
|
197
250
|
f"Author: {info['author']}\n"
|
|
@@ -31,7 +31,6 @@ class TranslationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
31
31
|
def init_ui(self):
|
|
32
32
|
self.setWindowTitle("Translation")
|
|
33
33
|
self.setModal(False) # モードレスにしてクリックを阻害しない
|
|
34
|
-
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint) # 常に前面表示
|
|
35
34
|
layout = QVBoxLayout(self)
|
|
36
35
|
|
|
37
36
|
# Instructions
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -3,57 +3,57 @@ moleditpy/__main__.py,sha256=bYu_h7lhc6PIl1-I_VI4_Q5MdBHmVrMiO4DYsy9jbiY,829
|
|
|
3
3
|
moleditpy/main.py,sha256=0D8_CXe4xdgqlu2TWoT2pi35K9y2b4ilFj6LWD9Qk7w,1178
|
|
4
4
|
moleditpy/modules/__init__.py,sha256=BndB5rKHIcdjtctu9csFxHm14khdUdN1BRO4aSzT3IQ,1727
|
|
5
5
|
moleditpy/modules/about_dialog.py,sha256=Edt09PZbsU9qd94tJCY57CDHNmRVFfa0DSPLgP7IGG0,3683
|
|
6
|
-
moleditpy/modules/align_plane_dialog.py,sha256=
|
|
7
|
-
moleditpy/modules/alignment_dialog.py,sha256=
|
|
6
|
+
moleditpy/modules/align_plane_dialog.py,sha256=SEC32l8z2x1W1Sf1Gu001OQEtinuvqGuCGHRELmtU5s,11834
|
|
7
|
+
moleditpy/modules/alignment_dialog.py,sha256=CPubSJI52mNSXmWt7oKQ2un5lXnN8_K4Z-LMM7wWTxQ,11309
|
|
8
8
|
moleditpy/modules/analysis_window.py,sha256=zjP5ipSTpKw8oLr1eKdoxW8Bk1SslGlPqsVucD-x_5w,9403
|
|
9
|
-
moleditpy/modules/angle_dialog.py,sha256=
|
|
9
|
+
moleditpy/modules/angle_dialog.py,sha256=uc2WbvSfRe892xoEirqpZ78pf2Smwzkinkso6zLWr0Y,17751
|
|
10
10
|
moleditpy/modules/atom_item.py,sha256=u8ge6B1M9sOGobfzg3tp1-EGXtEUmvdee7Fx6msg8Wk,15566
|
|
11
11
|
moleditpy/modules/bond_item.py,sha256=eVkEeKvM4igYI67DYxpey3FllqDyt_iWDo4VPYMhaPk,19137
|
|
12
|
-
moleditpy/modules/bond_length_dialog.py,sha256=
|
|
12
|
+
moleditpy/modules/bond_length_dialog.py,sha256=6bFPGssnqlgINuqpxLv-OhjMH3_hspnaH8QtorAyu2M,14782
|
|
13
13
|
moleditpy/modules/calculation_worker.py,sha256=KiGQY7i-QCQofEoE0r65KoQgpEGFcbhmxWv6egfkUdc,42324
|
|
14
14
|
moleditpy/modules/color_settings_dialog.py,sha256=Ow44BhCOLo0AFb6klO001k6B4drOgKX9DeNBQhZLp5o,15474
|
|
15
|
-
moleditpy/modules/constants.py,sha256=
|
|
16
|
-
moleditpy/modules/constrained_optimization_dialog.py,sha256=
|
|
17
|
-
moleditpy/modules/custom_interactor_style.py,sha256=
|
|
18
|
-
moleditpy/modules/custom_qt_interactor.py,sha256=
|
|
19
|
-
moleditpy/modules/dialog3_d_picking_mixin.py,sha256=
|
|
20
|
-
moleditpy/modules/dihedral_dialog.py,sha256=
|
|
15
|
+
moleditpy/modules/constants.py,sha256=J9rIFaD6tWxdMBzGcaYWovarEJy72gXD784k9WVahr8,4702
|
|
16
|
+
moleditpy/modules/constrained_optimization_dialog.py,sha256=REsk4ePsqNmAGPMTS_jckeM7jexrU3krwun8sKqKUCs,30062
|
|
17
|
+
moleditpy/modules/custom_interactor_style.py,sha256=LDNODMJoNHGe1AUSrvqv6PdeJm-hpPmSpWINppnJLt0,38942
|
|
18
|
+
moleditpy/modules/custom_qt_interactor.py,sha256=vCZsDfRO-FtphD5cTP7Ps-5rpHZMIGloaoe6EaKzrsw,4139
|
|
19
|
+
moleditpy/modules/dialog3_d_picking_mixin.py,sha256=z4udbkiX9PYmIGazPXsbftkk_oRRwZhcvlCqbyJzr24,6493
|
|
20
|
+
moleditpy/modules/dihedral_dialog.py,sha256=bOTDO6-b74vEDn_z6OyuBr5cRz3RnRj83PiaEBUyWJA,18002
|
|
21
21
|
moleditpy/modules/main_window.py,sha256=IL8dH3qPx2TkPgO7amuDgjlFoadh5J59xYUEVhlNZqA,36338
|
|
22
22
|
moleditpy/modules/main_window_app_state.py,sha256=8YDcGNCSpLTO1NGL9tEvNkXpUcS7JW-uK7TdUGvEqnk,35189
|
|
23
23
|
moleditpy/modules/main_window_compute.py,sha256=ipIkhH_DONXDnPzh7xeym9X-Yfx8EhsvXYOdyxsAj4c,53347
|
|
24
|
-
moleditpy/modules/main_window_dialog_manager.py,sha256=
|
|
24
|
+
moleditpy/modules/main_window_dialog_manager.py,sha256=QR96LqHAPSOShXbc9cK-Ffq8a16JrXAoMKB0pHjESrQ,20072
|
|
25
25
|
moleditpy/modules/main_window_edit_3d.py,sha256=CUArB5wcsgq1C7LygAEC6URlbnn4RhRYDa5n-Y-etWI,19731
|
|
26
|
-
moleditpy/modules/main_window_edit_actions.py,sha256
|
|
26
|
+
moleditpy/modules/main_window_edit_actions.py,sha256=yEc0Nw-VpN0P4e4neUu7pDuUHPGEcu6eFmwWFrSBIQ8,64815
|
|
27
27
|
moleditpy/modules/main_window_export.py,sha256=dSVfylsybDDboDuXU9Inotf6YkrKJwgBTqGYSfq1lRE,38241
|
|
28
|
-
moleditpy/modules/main_window_main_init.py,sha256=
|
|
28
|
+
moleditpy/modules/main_window_main_init.py,sha256=BChZuyXgWfOTFqAM1OUukFgtL5KLusW4t29GnfCN1QE,92018
|
|
29
29
|
moleditpy/modules/main_window_molecular_parsers.py,sha256=KR6vzuqc3nutOcorpYr0QOyX3MFBcxTwDhZX96VgJ9Q,48291
|
|
30
30
|
moleditpy/modules/main_window_project_io.py,sha256=TWwtuKDuvgcvPZ9IGmW8r1EJJOrgxrIJRnxe_f4C1oM,17149
|
|
31
31
|
moleditpy/modules/main_window_string_importers.py,sha256=v47wOd4RtjKYcF-aLP-mogGGdYTpTEo3dDyAu79_5MM,10782
|
|
32
32
|
moleditpy/modules/main_window_ui_manager.py,sha256=HofI6T9EvcSSzPbsdPqkYEEDoB6Hui1Uj2Ll-wwczGA,24016
|
|
33
|
-
moleditpy/modules/main_window_view_3d.py,sha256=
|
|
33
|
+
moleditpy/modules/main_window_view_3d.py,sha256=CxZxyJHl2isF7KtyVWSI9f8LVbvdZM5H9Gnhm_8ovBM,74227
|
|
34
34
|
moleditpy/modules/main_window_view_loaders.py,sha256=gklTMo27QnyJ8Gd0ampPdbm9d0Gi-oHWkIqQuGADHmI,14352
|
|
35
35
|
moleditpy/modules/mirror_dialog.py,sha256=c3v4qY6R4FAljzk4EPaDjL9ZdZMjLQSFLqDMXz2fBUk,4696
|
|
36
36
|
moleditpy/modules/molecular_data.py,sha256=8gE9ByYg3kSBfb1zANsyad_BVBTm6WOLF7NsZIYuG2E,13250
|
|
37
37
|
moleditpy/modules/molecule_scene.py,sha256=khdt7h9Mk_D1cMbYeHGtq7P9aFXo0xG-hcShU_H2Y-Q,95911
|
|
38
|
-
moleditpy/modules/move_group_dialog.py,sha256=
|
|
38
|
+
moleditpy/modules/move_group_dialog.py,sha256=Fyuy3Uq1KsFsk9qR96r_FxPbAM_-zSfW2dsMQGv7btc,27276
|
|
39
39
|
moleditpy/modules/periodic_table_dialog.py,sha256=ItEZUts1XCietz9paY-spvbzxh6SXak3GnikwqkHZCw,4006
|
|
40
|
-
moleditpy/modules/planarize_dialog.py,sha256=
|
|
40
|
+
moleditpy/modules/planarize_dialog.py,sha256=eaqI1MpF35e-VUMpJATt-EtGG5FhcSUlbAenUaFGabY,8593
|
|
41
41
|
moleditpy/modules/plugin_interface.py,sha256=srzPZ3a_aRTx28NAvWNKRVUDYQNfQOTcjzx-5YW2Pb4,8164
|
|
42
|
-
moleditpy/modules/plugin_manager.py,sha256=
|
|
43
|
-
moleditpy/modules/plugin_manager_window.py,sha256=
|
|
42
|
+
moleditpy/modules/plugin_manager.py,sha256=nb106AfUAnAVyp4LYFmLhKzFY4taeteH8gY5SNf1h1I,20037
|
|
43
|
+
moleditpy/modules/plugin_manager_window.py,sha256=b4kEv0DaWHZG76ZaFTOxn6CVtA62_0MpPYYr10ehCtA,12544
|
|
44
44
|
moleditpy/modules/settings_dialog.py,sha256=Nr7yE8UmYRi3VObWvRlrnv0DnjSjmYXbvqryZ02O12k,65348
|
|
45
45
|
moleditpy/modules/template_preview_item.py,sha256=djdq3tz73d_fJGOvai3E-V9Hk9q9ZW7skx7BV59mooA,6556
|
|
46
46
|
moleditpy/modules/template_preview_view.py,sha256=4OCHZDO51BvJpKdfrBWJ4_4WfLfFSKxsVIyf7I-Kj2E,3350
|
|
47
|
-
moleditpy/modules/translation_dialog.py,sha256=
|
|
47
|
+
moleditpy/modules/translation_dialog.py,sha256=x_GJsbVk-cj4aN2KgmYWDRUDInFlXezAoYoTvX-OT30,14553
|
|
48
48
|
moleditpy/modules/user_template_dialog.py,sha256=2hARO04DaILgdExx5ubL0GPsxK95VvVRqy7fNfudD_M,30843
|
|
49
49
|
moleditpy/modules/zoomable_view.py,sha256=hjwljui13QpvjvxJHY4Evot4jMQvxRBQUNH5HUlyFOk,5966
|
|
50
50
|
moleditpy/modules/assets/file_icon.ico,sha256=yyVj084A7HuMNbV073cE_Ag3Ne405qgOP3Mia1ZqLpE,101632
|
|
51
51
|
moleditpy/modules/assets/icon.icns,sha256=wD5R6-Vw7K662tVKhu2E1ImN0oUuyAP4youesEQsn9c,139863
|
|
52
52
|
moleditpy/modules/assets/icon.ico,sha256=RfgFcx7-dHY_2STdsOQCQziY5SNhDr3gPnjO6jzEDPI,147975
|
|
53
53
|
moleditpy/modules/assets/icon.png,sha256=kCFN1WacYIdy0GN6SFEbNA00ef39pCczBnFdkkBI8Bs,147110
|
|
54
|
-
moleditpy-2.
|
|
55
|
-
moleditpy-2.
|
|
56
|
-
moleditpy-2.
|
|
57
|
-
moleditpy-2.
|
|
58
|
-
moleditpy-2.
|
|
59
|
-
moleditpy-2.
|
|
54
|
+
moleditpy-2.3.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
55
|
+
moleditpy-2.3.0.dist-info/METADATA,sha256=KMJCk7r-QqD1yc1YHvb-EoQ65q5Xgfa9gvuO0sSmzEw,60629
|
|
56
|
+
moleditpy-2.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
57
|
+
moleditpy-2.3.0.dist-info/entry_points.txt,sha256=yH1h9JjALhok1foXT3-hYrC4ufoZt8b7oiBcsdnGNNM,54
|
|
58
|
+
moleditpy-2.3.0.dist-info/top_level.txt,sha256=ARICrS4ihlPXqywlKl6o-oJa3Qz3gZRWu_VZsQ3_c44,10
|
|
59
|
+
moleditpy-2.3.0.dist-info/RECORD,,
|
|
File without changes
|