MoleditPy-linux 2.4.1__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 (59) 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 +292 -0
  7. moleditpy_linux/modules/alignment_dialog.py +272 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/file_icon.ico +0 -0
  11. moleditpy_linux/modules/assets/icon.icns +0 -0
  12. moleditpy_linux/modules/assets/icon.ico +0 -0
  13. moleditpy_linux/modules/assets/icon.png +0 -0
  14. moleditpy_linux/modules/atom_item.py +395 -0
  15. moleditpy_linux/modules/bond_item.py +464 -0
  16. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  17. moleditpy_linux/modules/calculation_worker.py +766 -0
  18. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  19. moleditpy_linux/modules/constants.py +88 -0
  20. moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
  21. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  22. moleditpy_linux/modules/custom_qt_interactor.py +102 -0
  23. moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
  24. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  25. moleditpy_linux/modules/main_window.py +850 -0
  26. moleditpy_linux/modules/main_window_app_state.py +787 -0
  27. moleditpy_linux/modules/main_window_compute.py +1242 -0
  28. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  29. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  30. moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
  31. moleditpy_linux/modules/main_window_export.py +917 -0
  32. moleditpy_linux/modules/main_window_main_init.py +2100 -0
  33. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  34. moleditpy_linux/modules/main_window_project_io.py +434 -0
  35. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  36. moleditpy_linux/modules/main_window_ui_manager.py +602 -0
  37. moleditpy_linux/modules/main_window_view_3d.py +1539 -0
  38. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  39. moleditpy_linux/modules/mirror_dialog.py +122 -0
  40. moleditpy_linux/modules/molecular_data.py +302 -0
  41. moleditpy_linux/modules/molecule_scene.py +2000 -0
  42. moleditpy_linux/modules/move_group_dialog.py +600 -0
  43. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  44. moleditpy_linux/modules/planarize_dialog.py +220 -0
  45. moleditpy_linux/modules/plugin_interface.py +215 -0
  46. moleditpy_linux/modules/plugin_manager.py +473 -0
  47. moleditpy_linux/modules/plugin_manager_window.py +274 -0
  48. moleditpy_linux/modules/settings_dialog.py +1503 -0
  49. moleditpy_linux/modules/template_preview_item.py +157 -0
  50. moleditpy_linux/modules/template_preview_view.py +74 -0
  51. moleditpy_linux/modules/translation_dialog.py +364 -0
  52. moleditpy_linux/modules/user_template_dialog.py +692 -0
  53. moleditpy_linux/modules/zoomable_view.py +129 -0
  54. moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
  55. moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
  56. moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
  57. moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
  58. moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
  59. moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,274 @@
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
+ import shutil
22
+
23
+ class PluginManagerWindow(QDialog):
24
+ def __init__(self, plugin_manager, parent=None):
25
+ super().__init__(parent)
26
+ self.plugin_manager = plugin_manager
27
+ self.setWindowTitle("Plugin Manager")
28
+ self.resize(800, 500)
29
+ self.setAcceptDrops(True) # Enable drag & drop for the whole window
30
+
31
+ self.init_ui()
32
+ self.refresh_plugin_list()
33
+
34
+ def init_ui(self):
35
+ layout = QVBoxLayout(self)
36
+
37
+ lbl_info = QLabel("Drag & Drop .py or .zip 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
+ # 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}")
180
+ else:
181
+ QMessageBox.warning(self, "Error", f"Plugin file not found:\n{filepath}")
182
+
183
+ def show_plugin_details(self, item):
184
+ row = item.row()
185
+ if row < len(self.plugin_manager.plugins):
186
+ p = self.plugin_manager.plugins[row]
187
+ msg = f"Name: {p.get('name', 'Unknown')}\n" \
188
+ f"Version: {p.get('version', 'Unknown')}\n" \
189
+ f"Author: {p.get('author', 'Unknown')}\n" \
190
+ f"Status: {p.get('status', 'Unknown')}\n" \
191
+ f"Location: {p.get('filepath', 'Unknown')}\n\n" \
192
+ f"Description:\n{p.get('description', 'No description available.')}"
193
+ QMessageBox.information(self, "Plugin Details", msg)
194
+
195
+ # --- Drag & Drop Support ---
196
+ def dragEnterEvent(self, event: QDragEnterEvent):
197
+ if event.mimeData().hasUrls():
198
+ event.accept()
199
+ else:
200
+ event.ignore()
201
+
202
+ def dropEvent(self, event: QDropEvent):
203
+ files_installed = []
204
+ errors = []
205
+ for url in event.mimeData().urls():
206
+ file_path = url.toLocalFile()
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:
232
+ # Extract info and confirm
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
+
248
+ msg = (f"Do you want to install this plugin?\n\n"
249
+ f"Name: {info['name']}\n"
250
+ f"Author: {info['author']}\n"
251
+ f"Version: {info['version']}\n"
252
+ f"Description: {info['description']}\n\n"
253
+ f"File: {os.path.basename(file_path)}")
254
+
255
+ reply = QMessageBox.question(self, "Install Plugin?", msg,
256
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
257
+
258
+ if reply == QMessageBox.StandardButton.Yes:
259
+ success, msg = self.plugin_manager.install_plugin(file_path)
260
+ if success:
261
+ files_installed.append(msg)
262
+ else:
263
+ errors.append(msg)
264
+
265
+ if files_installed or errors:
266
+ self.refresh_plugin_list()
267
+ summary = ""
268
+ if files_installed:
269
+ summary += "Installed:\n" + "\n".join(files_installed) + "\n\n"
270
+ if errors:
271
+ summary += "Errors:\n" + "\n".join(errors)
272
+
273
+ QMessageBox.information(self, "Plugin Installation", summary)
274
+