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.
- 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 +292 -0
- moleditpy_linux/modules/alignment_dialog.py +272 -0
- moleditpy_linux/modules/analysis_window.py +209 -0
- moleditpy_linux/modules/angle_dialog.py +440 -0
- moleditpy_linux/modules/assets/file_icon.ico +0 -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 +395 -0
- moleditpy_linux/modules/bond_item.py +464 -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 +678 -0
- moleditpy_linux/modules/custom_interactor_style.py +749 -0
- moleditpy_linux/modules/custom_qt_interactor.py +102 -0
- moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
- moleditpy_linux/modules/dihedral_dialog.py +443 -0
- moleditpy_linux/modules/main_window.py +850 -0
- moleditpy_linux/modules/main_window_app_state.py +787 -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 +1565 -0
- moleditpy_linux/modules/main_window_export.py +917 -0
- moleditpy_linux/modules/main_window_main_init.py +2100 -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 +602 -0
- moleditpy_linux/modules/main_window_view_3d.py +1539 -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 +600 -0
- moleditpy_linux/modules/periodic_table_dialog.py +84 -0
- moleditpy_linux/modules/planarize_dialog.py +220 -0
- moleditpy_linux/modules/plugin_interface.py +215 -0
- moleditpy_linux/modules/plugin_manager.py +473 -0
- moleditpy_linux/modules/plugin_manager_window.py +274 -0
- moleditpy_linux/modules/settings_dialog.py +1503 -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 +364 -0
- moleditpy_linux/modules/user_template_dialog.py +692 -0
- moleditpy_linux/modules/zoomable_view.py +129 -0
- moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
- moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
- moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
- moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
- moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
- 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
|
+
|