MoleditPy-linux 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.
Files changed (25) hide show
  1. moleditpy_linux/modules/align_plane_dialog.py +0 -1
  2. moleditpy_linux/modules/alignment_dialog.py +0 -1
  3. moleditpy_linux/modules/angle_dialog.py +1 -1
  4. moleditpy_linux/modules/bond_length_dialog.py +1 -1
  5. moleditpy_linux/modules/constants.py +1 -1
  6. moleditpy_linux/modules/constrained_optimization_dialog.py +0 -1
  7. moleditpy_linux/modules/custom_interactor_style.py +56 -56
  8. moleditpy_linux/modules/custom_qt_interactor.py +43 -0
  9. moleditpy_linux/modules/dialog3_d_picking_mixin.py +42 -9
  10. moleditpy_linux/modules/dihedral_dialog.py +1 -1
  11. moleditpy_linux/modules/main_window_dialog_manager.py +9 -9
  12. moleditpy_linux/modules/main_window_edit_actions.py +4 -1
  13. moleditpy_linux/modules/main_window_main_init.py +51 -27
  14. moleditpy_linux/modules/main_window_view_3d.py +9 -1
  15. moleditpy_linux/modules/move_group_dialog.py +7 -5
  16. moleditpy_linux/modules/planarize_dialog.py +0 -1
  17. moleditpy_linux/modules/plugin_manager.py +234 -95
  18. moleditpy_linux/modules/plugin_manager_window.py +67 -14
  19. moleditpy_linux/modules/translation_dialog.py +0 -1
  20. {moleditpy_linux-2.2.5.dist-info → moleditpy_linux-2.3.0.dist-info}/METADATA +2 -2
  21. {moleditpy_linux-2.2.5.dist-info → moleditpy_linux-2.3.0.dist-info}/RECORD +25 -25
  22. {moleditpy_linux-2.2.5.dist-info → moleditpy_linux-2.3.0.dist-info}/WHEEL +0 -0
  23. {moleditpy_linux-2.2.5.dist-info → moleditpy_linux-2.3.0.dist-info}/entry_points.txt +0 -0
  24. {moleditpy_linux-2.2.5.dist-info → moleditpy_linux-2.3.0.dist-info}/licenses/LICENSE +0 -0
  25. {moleditpy_linux-2.2.5.dist-info → moleditpy_linux-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
- atomic_num = atom.GetAtomicNum()
199
- pt = Chem.GetPeriodicTable()
200
- vdw_radius = pt.GetRvdw(atomic_num)
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 False
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
- dest_path = os.path.join(self.plugin_dir, filename)
79
- shutil.copy2(file_path, dest_path)
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, f"Installed {filename}"
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
- Recursively scans the plugin directory.
90
- Supports both legacy autorun(parent) and new initialize(context).
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
- # Modify dirs in-place to skip hidden directories and __pycache__
191
+ # Exclude hidden directories
115
192
  dirs[:] = [d for d in dirs if not d.startswith('__') and d != '__pycache__']
116
193
 
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 = ""
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
- 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
-
274
+ module.initialize(context)
184
275
  except Exception as e:
185
- print(f"Failed to load plugin {filename}: {e}")
276
+ status = f"Error (Init): {e}"
277
+ print(f"Plugin {plugin_name} initialize error: {e}")
186
278
  traceback.print_exc()
187
-
188
- return self.plugins
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
- for target in node.targets:
271
- if isinstance(target, ast.Name):
272
- # Helper to extract value
273
- val = None
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
- 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
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
- # Header / Instruction
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
- 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}")
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
- if os.path.isfile(file_path) and file_path.endswith('.py'):
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 = self.plugin_manager.get_plugin_info_safe(file_path)
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-linux
3
- Version: 2.2.5
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
@@ -689,7 +689,7 @@ Classifier: Programming Language :: Python :: 3.10
689
689
  Classifier: Programming Language :: Python :: 3.11
690
690
  Classifier: Programming Language :: Python :: 3.12
691
691
  Classifier: Programming Language :: Python :: 3.13
692
- Requires-Python: <3.14,>=3.9
692
+ Requires-Python: <3.15,>=3.9
693
693
  Description-Content-Type: text/markdown
694
694
  License-File: LICENSE
695
695
  Requires-Dist: numpy