MoleditPy 4.10.2__tar.gz → 4.10.3__tar.gz
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-4.10.2 → moleditpy-4.10.3}/PKG-INFO +7 -3
- {moleditpy-4.10.2 → moleditpy-4.10.3}/README.md +6 -2
- {moleditpy-4.10.2 → moleditpy-4.10.3}/pyproject.toml +1 -1
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/PKG-INFO +7 -3
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/main.py +3 -1
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_manager.py +9 -5
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_manager_window.py +50 -38
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/compute_logic.py +9 -6
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/io_logic.py +44 -11
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/main_window_init.py +15 -10
- moleditpy-4.10.3/src/moleditpy/ui/move_dialog_mixin.py +250 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/move_group_dialog.py +2 -209
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/move_selected_atoms_dialog.py +17 -240
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/plugin_menu_manager.py +7 -8
- {moleditpy-4.10.2 → moleditpy-4.10.3}/LICENSE +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/setup.cfg +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/__init__.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/__main__.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/core/__init__.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/core/mol_geometry.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/core/molecular_data.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/plugins/__init__.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_interface.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/__init__.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/about_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/align_plane_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/alignment_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/analysis_window.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/angle_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/app_state.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/atom_item.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/atom_picking.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/base_picking_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/bond_item.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/bond_length_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/calculation_worker.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/chain_mixin.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/color_settings_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/custom_interactor_style.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/dialog_logic.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/dihedral_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/edit_3d_logic.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/edit_actions_logic.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/export_logic.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/geometry_base_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/main_window.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/mirror_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/molecular_scene_handler.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/molecule_scene.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/planarize_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/preview_molecule.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/string_importers.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/template_preview_item.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/template_preview_view.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/translation_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/ui_manager.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/user_template_dialog.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/view_3d_logic.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/zoomable_view.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/__init__.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/constants.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/default_settings.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/hit_radius.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/suppress_log.py +0 -0
- {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/system_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 4.10.
|
|
3
|
+
Version: 4.10.3
|
|
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
|
|
@@ -705,9 +705,10 @@ Dynamic: license-file
|
|
|
705
705
|
# MoleditPy — A Python Molecular Editor
|
|
706
706
|
|
|
707
707
|
[](https://doi.org/10.5281/zenodo.17268532)
|
|
708
|
+

|
|
708
709
|
[](https://www.rdkit.org/)
|
|
709
710
|
[](https://badge.fury.io/py/MoleditPy)
|
|
710
|
-
[](https://pypi.org/project/MoleditPy/)
|
|
711
712
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
712
713
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
713
714
|
[](https://codecov.io/gh/HiroYokoyama/python_molecular_editor)
|
|
@@ -727,7 +728,10 @@ Dynamic: license-file
|
|
|
727
728
|
**Repository**: [https://github.com/HiroYokoyama/python\_molecular\_editor](https://github.com/HiroYokoyama/python_molecular_editor)
|
|
728
729
|
|
|
729
730
|
-----
|
|
730
|
-
|
|
731
|
+
<p align="center">
|
|
732
|
+
<img src="img/icon.png" width="500" alt="MoleditPy Icon">
|
|
733
|
+
</p>
|
|
734
|
+
|
|
731
735
|

|
|
732
736
|
|
|
733
737
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# MoleditPy — A Python Molecular Editor
|
|
2
2
|
|
|
3
3
|
[](https://doi.org/10.5281/zenodo.17268532)
|
|
4
|
+

|
|
4
5
|
[](https://www.rdkit.org/)
|
|
5
6
|
[](https://badge.fury.io/py/MoleditPy)
|
|
6
|
-
[](https://pypi.org/project/MoleditPy/)
|
|
7
8
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
8
9
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
9
10
|
[](https://codecov.io/gh/HiroYokoyama/python_molecular_editor)
|
|
@@ -23,7 +24,10 @@
|
|
|
23
24
|
**Repository**: [https://github.com/HiroYokoyama/python\_molecular\_editor](https://github.com/HiroYokoyama/python_molecular_editor)
|
|
24
25
|
|
|
25
26
|
-----
|
|
26
|
-
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="img/icon.png" width="500" alt="MoleditPy Icon">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
27
31
|

|
|
28
32
|
|
|
29
33
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 4.10.
|
|
3
|
+
Version: 4.10.3
|
|
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
|
|
@@ -705,9 +705,10 @@ Dynamic: license-file
|
|
|
705
705
|
# MoleditPy — A Python Molecular Editor
|
|
706
706
|
|
|
707
707
|
[](https://doi.org/10.5281/zenodo.17268532)
|
|
708
|
+

|
|
708
709
|
[](https://www.rdkit.org/)
|
|
709
710
|
[](https://badge.fury.io/py/MoleditPy)
|
|
710
|
-
[](https://pypi.org/project/MoleditPy/)
|
|
711
712
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
712
713
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
713
714
|
[](https://codecov.io/gh/HiroYokoyama/python_molecular_editor)
|
|
@@ -727,7 +728,10 @@ Dynamic: license-file
|
|
|
727
728
|
**Repository**: [https://github.com/HiroYokoyama/python\_molecular\_editor](https://github.com/HiroYokoyama/python_molecular_editor)
|
|
728
729
|
|
|
729
730
|
-----
|
|
730
|
-
|
|
731
|
+
<p align="center">
|
|
732
|
+
<img src="img/icon.png" width="500" alt="MoleditPy Icon">
|
|
733
|
+
</p>
|
|
734
|
+
|
|
731
735
|

|
|
732
736
|
|
|
733
737
|
|
|
@@ -53,6 +53,7 @@ src/moleditpy/ui/main_window_init.py
|
|
|
53
53
|
src/moleditpy/ui/mirror_dialog.py
|
|
54
54
|
src/moleditpy/ui/molecular_scene_handler.py
|
|
55
55
|
src/moleditpy/ui/molecule_scene.py
|
|
56
|
+
src/moleditpy/ui/move_dialog_mixin.py
|
|
56
57
|
src/moleditpy/ui/move_group_dialog.py
|
|
57
58
|
src/moleditpy/ui/move_selected_atoms_dialog.py
|
|
58
59
|
src/moleditpy/ui/periodic_table_dialog.py
|
|
@@ -280,7 +280,9 @@ def main() -> None:
|
|
|
280
280
|
metadata_file = init_py
|
|
281
281
|
|
|
282
282
|
info = (
|
|
283
|
-
pm.get_plugin_info_safe(
|
|
283
|
+
pm.get_plugin_info_safe(
|
|
284
|
+
metadata_file, fallback_name=os.path.basename(plugin_path)
|
|
285
|
+
)
|
|
284
286
|
if metadata_file.endswith(".py")
|
|
285
287
|
else {}
|
|
286
288
|
)
|
|
@@ -348,10 +348,11 @@ class PluginManager:
|
|
|
348
348
|
self, filepath: str, module_name: str, category: str
|
|
349
349
|
) -> None:
|
|
350
350
|
"""Add a disabled plugin without importing or initializing its code."""
|
|
351
|
-
|
|
351
|
+
# Same fallback as _load_single_plugin's getattr(..., module_name).
|
|
352
|
+
info = self.get_plugin_info_safe(filepath, fallback_name=module_name)
|
|
352
353
|
self.plugins.append(
|
|
353
354
|
{
|
|
354
|
-
"name": info
|
|
355
|
+
"name": info["name"],
|
|
355
356
|
"version": info.get("version", "Unknown"),
|
|
356
357
|
"author": info.get("author", "Unknown"),
|
|
357
358
|
"description": info.get("description", ""),
|
|
@@ -800,10 +801,13 @@ class PluginManager:
|
|
|
800
801
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
801
802
|
return False
|
|
802
803
|
|
|
803
|
-
def get_plugin_info_safe(
|
|
804
|
-
|
|
804
|
+
def get_plugin_info_safe(
|
|
805
|
+
self, file_path: str, fallback_name: Optional[str] = None
|
|
806
|
+
) -> Dict[str, str]:
|
|
807
|
+
"""Extract plugin metadata by AST parsing; ``fallback_name`` stands in
|
|
808
|
+
for a missing PLUGIN_NAME, defaulting to the file's basename."""
|
|
805
809
|
info = {
|
|
806
|
-
"name": os.path.basename(file_path),
|
|
810
|
+
"name": fallback_name or os.path.basename(file_path),
|
|
807
811
|
"version": "Unknown",
|
|
808
812
|
"author": "Unknown",
|
|
809
813
|
"description": "",
|
|
@@ -115,7 +115,8 @@ class PluginManagerWindow(QDialog):
|
|
|
115
115
|
btn_layout = QHBoxLayout()
|
|
116
116
|
|
|
117
117
|
btn_reload = QPushButton("Reload Plugins")
|
|
118
|
-
|
|
118
|
+
# Not connect(self.on_reload): clicked(bool) would land in *silent*.
|
|
119
|
+
btn_reload.clicked.connect(lambda: self.on_reload())
|
|
119
120
|
btn_layout.addWidget(btn_reload)
|
|
120
121
|
|
|
121
122
|
btn_folder = QPushButton("Open Plugin Folder")
|
|
@@ -144,8 +145,8 @@ class PluginManagerWindow(QDialog):
|
|
|
144
145
|
|
|
145
146
|
def _create_checkbox_container(
|
|
146
147
|
self, checked: bool, tooltip: str = "Enable or disable this plugin"
|
|
147
|
-
) ->
|
|
148
|
-
"""Create a centered QCheckBox container
|
|
148
|
+
) -> QWidget:
|
|
149
|
+
"""Create a centered QCheckBox container; read it back via _status_checkbox."""
|
|
149
150
|
container = QWidget()
|
|
150
151
|
box_layout = QHBoxLayout(container)
|
|
151
152
|
box_layout.setContentsMargins(0, 0, 0, 0)
|
|
@@ -154,7 +155,7 @@ class PluginManagerWindow(QDialog):
|
|
|
154
155
|
checkbox.setChecked(checked)
|
|
155
156
|
checkbox.setToolTip(tooltip)
|
|
156
157
|
box_layout.addWidget(checkbox)
|
|
157
|
-
return container
|
|
158
|
+
return container
|
|
158
159
|
|
|
159
160
|
def refresh_plugin_list(self) -> None:
|
|
160
161
|
"""Repopulate the plugin table from the current plugin registry."""
|
|
@@ -165,13 +166,12 @@ class PluginManagerWindow(QDialog):
|
|
|
165
166
|
for row, p in enumerate(plugins):
|
|
166
167
|
# Column 0: Enabled Checkbox (Clean native check, centered)
|
|
167
168
|
is_enabled = not p.get("disabled", False)
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
self.table.setCellWidget(
|
|
170
|
+
row, 0, self._create_checkbox_container(is_enabled)
|
|
171
|
+
)
|
|
170
172
|
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
enabled_item.setData(Qt.ItemDataRole.UserRole, row)
|
|
174
|
-
self.table.setItem(row, 0, enabled_item)
|
|
173
|
+
# Empty item behind the widget so the column still selects the row.
|
|
174
|
+
self.table.setItem(row, 0, QTableWidgetItem())
|
|
175
175
|
|
|
176
176
|
# Column 1: Status (colored text)
|
|
177
177
|
status = str(p.get("status", "Unknown"))
|
|
@@ -254,6 +254,16 @@ class PluginManagerWindow(QDialog):
|
|
|
254
254
|
)
|
|
255
255
|
self.table.setRowHidden(row, not match)
|
|
256
256
|
|
|
257
|
+
self._drop_hidden_selection()
|
|
258
|
+
|
|
259
|
+
def _drop_hidden_selection(self) -> None:
|
|
260
|
+
"""Clear the selection when the filter hides the row it points at."""
|
|
261
|
+
row = self.table.currentRow()
|
|
262
|
+
if row >= 0 and self.table.isRowHidden(row):
|
|
263
|
+
self.table.clearSelection()
|
|
264
|
+
self.table.setCurrentCell(-1, -1)
|
|
265
|
+
self.update_button_state()
|
|
266
|
+
|
|
257
267
|
def _status_checkbox(self, row: int) -> Optional[QCheckBox]:
|
|
258
268
|
"""Return the visible status checkbox for a plugin table row."""
|
|
259
269
|
container = self.table.cellWidget(row, 0)
|
|
@@ -265,22 +275,17 @@ class PluginManagerWindow(QDialog):
|
|
|
265
275
|
return None
|
|
266
276
|
|
|
267
277
|
def _save_checkbox_preferences(self) -> set[str]:
|
|
268
|
-
"""Collect and persist checkbox choices."""
|
|
278
|
+
"""Collect and persist checkbox choices, hidden rows included."""
|
|
269
279
|
disabled_paths = set()
|
|
270
280
|
for row in range(self.table.rowCount()):
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
):
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
filepath = plugin.get("filepath")
|
|
280
|
-
if filepath:
|
|
281
|
-
disabled_paths.add(
|
|
282
|
-
self.plugin_manager.plugin_path_key(filepath)
|
|
283
|
-
)
|
|
281
|
+
plugin = self._plugin_for_row(row)
|
|
282
|
+
if plugin is None:
|
|
283
|
+
continue
|
|
284
|
+
status_checkbox = self._status_checkbox(row)
|
|
285
|
+
if status_checkbox is not None and not status_checkbox.isChecked():
|
|
286
|
+
filepath = plugin.get("filepath")
|
|
287
|
+
if filepath:
|
|
288
|
+
disabled_paths.add(self.plugin_manager.plugin_path_key(filepath))
|
|
284
289
|
|
|
285
290
|
self.plugin_manager.save_disabled_plugins(disabled_paths)
|
|
286
291
|
return disabled_paths
|
|
@@ -299,9 +304,12 @@ class PluginManagerWindow(QDialog):
|
|
|
299
304
|
self.plugin_manager.discover_plugins()
|
|
300
305
|
|
|
301
306
|
def done(self, result: int) -> None:
|
|
302
|
-
"""Apply plugin preferences for every way the dialog can close.
|
|
303
|
-
|
|
307
|
+
"""Apply plugin preferences for every way the dialog can close.
|
|
308
|
+
|
|
309
|
+
Close first: applying them re-runs every plugin's ``initialize()``.
|
|
310
|
+
"""
|
|
304
311
|
super().done(result)
|
|
312
|
+
self._apply_plugin_preferences()
|
|
305
313
|
|
|
306
314
|
def update_button_state(self) -> None:
|
|
307
315
|
"""Enable or disable the Remove button based on table selection."""
|
|
@@ -324,17 +332,15 @@ class PluginManagerWindow(QDialog):
|
|
|
324
332
|
self.refresh_plugin_list()
|
|
325
333
|
|
|
326
334
|
def _plugin_for_row(self, row: int) -> Optional[dict[str, Any]]:
|
|
327
|
-
"""Retrieve plugin dict
|
|
328
|
-
|
|
335
|
+
"""Retrieve the plugin dict a table row was built from.
|
|
336
|
+
|
|
337
|
+
Row index is the plugin index; filtering hides rows, never reorders.
|
|
338
|
+
"""
|
|
339
|
+
if row < 0 or row >= min(
|
|
340
|
+
self.table.rowCount(), len(self.plugin_manager.plugins)
|
|
341
|
+
):
|
|
329
342
|
return None
|
|
330
|
-
|
|
331
|
-
idx = item.data(Qt.ItemDataRole.UserRole) if item else row
|
|
332
|
-
if idx is not None and 0 <= idx < len(self.plugin_manager.plugins):
|
|
333
|
-
plugin: dict[str, Any] = cast(
|
|
334
|
-
dict[str, Any], self.plugin_manager.plugins[idx]
|
|
335
|
-
)
|
|
336
|
-
return plugin
|
|
337
|
-
return None
|
|
343
|
+
return cast(dict[str, Any], self.plugin_manager.plugins[row])
|
|
338
344
|
|
|
339
345
|
def on_remove_plugin(self) -> None:
|
|
340
346
|
"""Delete the selected plugin file or folder and reload."""
|
|
@@ -456,8 +462,14 @@ class PluginManagerWindow(QDialog):
|
|
|
456
462
|
# Try to parse __init__.py if it exists
|
|
457
463
|
init_path = os.path.join(file_path, "__init__.py")
|
|
458
464
|
if os.path.exists(init_path):
|
|
459
|
-
|
|
460
|
-
info
|
|
465
|
+
folder_note = info["description"]
|
|
466
|
+
info = self.plugin_manager.get_plugin_info_safe(
|
|
467
|
+
init_path, fallback_name=os.path.basename(file_path)
|
|
468
|
+
)
|
|
469
|
+
declared = info["description"]
|
|
470
|
+
info["description"] = f"{folder_note} (Package: {info['name']})"
|
|
471
|
+
if declared:
|
|
472
|
+
info["description"] += f" - {declared}"
|
|
461
473
|
|
|
462
474
|
elif is_zip:
|
|
463
475
|
info["description"] = "ZIP Package Plugin"
|
|
@@ -66,12 +66,12 @@ class ComputeManager:
|
|
|
66
66
|
self._active_calc_threads = []
|
|
67
67
|
|
|
68
68
|
def _safe_disconnect(self, signal: Any) -> None:
|
|
69
|
-
"""
|
|
69
|
+
"""Disconnect a signal, tolerating one that has nothing connected."""
|
|
70
70
|
try:
|
|
71
71
|
signal.disconnect()
|
|
72
|
-
except RuntimeError:
|
|
73
|
-
#
|
|
74
|
-
logging.debug("
|
|
72
|
+
except (RuntimeError, TypeError):
|
|
73
|
+
# PyQt raises TypeError, not RuntimeError, when nothing is attached.
|
|
74
|
+
logging.debug("Nothing to disconnect from %s", signal, exc_info=True)
|
|
75
75
|
|
|
76
76
|
def _remove_calculating_text(self) -> None:
|
|
77
77
|
"""Safely remove the 'Calculating...' text actor from the plotter."""
|
|
@@ -90,14 +90,17 @@ class ComputeManager:
|
|
|
90
90
|
"""Restore the Convert and Optimize buttons to their default state."""
|
|
91
91
|
self._safe_disconnect(self.host.init_manager.convert_button.clicked) # type: ignore[union-attr]
|
|
92
92
|
self.host.init_manager.convert_button.setText("Convert 2D to 3D") # type: ignore[union-attr]
|
|
93
|
-
|
|
93
|
+
# Wrapped: clicked(bool) would otherwise arrive as conversion_mode.
|
|
94
|
+
self.host.init_manager.convert_button.clicked.connect( # type: ignore[union-attr]
|
|
95
|
+
lambda: self.trigger_conversion()
|
|
96
|
+
)
|
|
94
97
|
self.host.init_manager.convert_button.setEnabled(True) # type: ignore[union-attr]
|
|
95
98
|
|
|
96
99
|
if self.host.init_manager.optimize_3d_button:
|
|
97
100
|
self._safe_disconnect(self.host.init_manager.optimize_3d_button.clicked)
|
|
98
101
|
self.host.init_manager.optimize_3d_button.setText("Optimize 3D")
|
|
99
102
|
self.host.init_manager.optimize_3d_button.clicked.connect(
|
|
100
|
-
self.optimize_3d_structure
|
|
103
|
+
lambda: self.optimize_3d_structure()
|
|
101
104
|
)
|
|
102
105
|
self.host.init_manager.optimize_3d_button.setEnabled(True)
|
|
103
106
|
|
|
@@ -16,6 +16,7 @@ import logging
|
|
|
16
16
|
import os
|
|
17
17
|
import json
|
|
18
18
|
import pickle
|
|
19
|
+
import unicodedata
|
|
19
20
|
from ..utils.suppress_log import suppress_log
|
|
20
21
|
from typing import Any, Callable, List, Optional, Tuple
|
|
21
22
|
|
|
@@ -33,6 +34,8 @@ from PyQt6.QtWidgets import (
|
|
|
33
34
|
QWidget,
|
|
34
35
|
)
|
|
35
36
|
|
|
37
|
+
from io import BytesIO
|
|
38
|
+
|
|
36
39
|
from rdkit import Chem
|
|
37
40
|
from rdkit.Chem import (
|
|
38
41
|
Descriptors,
|
|
@@ -143,6 +146,23 @@ class IOManager:
|
|
|
143
146
|
raw = self._first_sdf_record(raw)
|
|
144
147
|
return self.fix_mol_block(raw)
|
|
145
148
|
|
|
149
|
+
def _read_mol_or_sdf(self, file_path: str) -> Optional[Chem.Mol]:
|
|
150
|
+
"""Read the molecule from a .mol or .sdf file, whatever its encoding.
|
|
151
|
+
|
|
152
|
+
The SDF goes through a stream, not a path, so both the encoding
|
|
153
|
+
fallbacks and the record's data fields survive.
|
|
154
|
+
"""
|
|
155
|
+
if file_path.lower().endswith(".sdf"):
|
|
156
|
+
raw = "".join(self._read_text_lines_flexible(file_path))
|
|
157
|
+
stream = BytesIO(self.fix_mol_block(raw).encode("utf-8"))
|
|
158
|
+
supplier = Chem.ForwardSDMolSupplier(stream, removeHs=False)
|
|
159
|
+
mol = next(supplier, None)
|
|
160
|
+
if mol is not None:
|
|
161
|
+
return mol
|
|
162
|
+
return Chem.MolFromMolBlock(
|
|
163
|
+
self._load_mol_block_text(file_path), sanitize=True, removeHs=False
|
|
164
|
+
)
|
|
165
|
+
|
|
146
166
|
@staticmethod
|
|
147
167
|
def _read_text_lines_flexible(file_path: str) -> list[str]:
|
|
148
168
|
"""Read a text file's lines, tolerating non-UTF-8 encodings.
|
|
@@ -159,12 +179,29 @@ class IOManager:
|
|
|
159
179
|
# some genuinely EUC-JP byte sequences and decodes them to the wrong
|
|
160
180
|
# (mojibake) characters instead of raising, so trying cp932 first
|
|
161
181
|
# would never let euc_jp text reach its correct decoding.
|
|
162
|
-
|
|
182
|
+
mojibake: Optional[List[str]] = None
|
|
183
|
+
cp932_gaiji: Optional[List[str]] = None
|
|
184
|
+
for encoding in ("utf-8-sig", "euc_jp", "cp932", "shift_jis", "cp1252"):
|
|
163
185
|
try:
|
|
164
186
|
with open(file_path, "r", encoding=encoding) as f:
|
|
165
|
-
|
|
187
|
+
lines = f.readlines()
|
|
166
188
|
except UnicodeDecodeError:
|
|
167
189
|
continue
|
|
190
|
+
# A wrong codec can land bytes in the private-use area. Keep looking,
|
|
191
|
+
# but preserve CP932's F0-lead gaiji range: CP1252 can otherwise
|
|
192
|
+
# decode bytes such as F0 40 cleanly while losing U+E000.
|
|
193
|
+
if any(unicodedata.category(ch) == "Co" for line in lines for ch in line):
|
|
194
|
+
if encoding == "cp932" and any(
|
|
195
|
+
"\ue000" <= ch <= "\ue0bb" for line in lines for ch in line
|
|
196
|
+
):
|
|
197
|
+
cp932_gaiji = lines
|
|
198
|
+
mojibake = mojibake if mojibake is not None else lines
|
|
199
|
+
continue
|
|
200
|
+
if encoding == "cp1252" and cp932_gaiji is not None:
|
|
201
|
+
return cp932_gaiji
|
|
202
|
+
return lines
|
|
203
|
+
if mojibake is not None:
|
|
204
|
+
return mojibake
|
|
168
205
|
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
169
206
|
return f.readlines()
|
|
170
207
|
|
|
@@ -839,13 +876,10 @@ class IOManager:
|
|
|
839
876
|
return
|
|
840
877
|
|
|
841
878
|
try:
|
|
842
|
-
|
|
843
|
-
#
|
|
844
|
-
#
|
|
845
|
-
|
|
846
|
-
mol: Optional[Chem.Mol] = Chem.MolFromMolBlock(
|
|
847
|
-
fixed_block, sanitize=True, removeHs=False
|
|
848
|
-
)
|
|
879
|
+
# Both readers can come back empty on a malformed file; the stubs
|
|
880
|
+
# do not say so, hence the explicit declaration. The None check
|
|
881
|
+
# below covers that.
|
|
882
|
+
mol: Optional[Chem.Mol] = self._read_mol_or_sdf(file_path)
|
|
849
883
|
|
|
850
884
|
if mol is None:
|
|
851
885
|
raise ValueError("Failed to read molecule from file.")
|
|
@@ -1047,8 +1081,7 @@ class IOManager:
|
|
|
1047
1081
|
if not file_path:
|
|
1048
1082
|
return
|
|
1049
1083
|
try:
|
|
1050
|
-
|
|
1051
|
-
mol = Chem.MolFromMolBlock(fixed_block, sanitize=True, removeHs=False)
|
|
1084
|
+
mol = self._read_mol_or_sdf(file_path)
|
|
1052
1085
|
|
|
1053
1086
|
if mol is None:
|
|
1054
1087
|
raise ValueError("Failed to load molecule.")
|
|
@@ -681,8 +681,9 @@ class MainInitManager:
|
|
|
681
681
|
left_buttons_layout.addWidget(self.cleanup_button)
|
|
682
682
|
|
|
683
683
|
self.convert_button = QPushButton("Convert 2D to 3D")
|
|
684
|
+
# Wrapped: clicked(bool) would otherwise arrive as conversion_mode.
|
|
684
685
|
self.convert_button.clicked.connect(
|
|
685
|
-
self.host.compute_manager.trigger_conversion
|
|
686
|
+
lambda: self.host.compute_manager.trigger_conversion()
|
|
686
687
|
)
|
|
687
688
|
# Allow right-click to open a temporary conversion-mode menu
|
|
688
689
|
try:
|
|
@@ -726,7 +727,7 @@ class MainInitManager:
|
|
|
726
727
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
|
|
727
728
|
)
|
|
728
729
|
self.optimize_3d_button.clicked.connect(
|
|
729
|
-
self.host.compute_manager.optimize_3d_structure
|
|
730
|
+
lambda: self.host.compute_manager.optimize_3d_structure()
|
|
730
731
|
)
|
|
731
732
|
self.optimize_3d_button.setEnabled(False)
|
|
732
733
|
# Initialized via _enable_3d_features(False)
|
|
@@ -979,7 +980,7 @@ class MainInitManager:
|
|
|
979
980
|
if key == "ball_and_stick":
|
|
980
981
|
action.setChecked(True)
|
|
981
982
|
action.triggered.connect(
|
|
982
|
-
lambda checked=False, k=key:
|
|
983
|
+
lambda checked=False, k=key: self.host.view_3d_manager.set_3d_style(k)
|
|
983
984
|
)
|
|
984
985
|
style_menu.addAction(action)
|
|
985
986
|
style_group.addAction(action)
|
|
@@ -1392,9 +1393,11 @@ class MainInitManager:
|
|
|
1392
1393
|
view_menu.addSeparator()
|
|
1393
1394
|
reset_3d_view_action = QAction("Reset 3D View", self.host)
|
|
1394
1395
|
reset_3d_view_action.triggered.connect(
|
|
1395
|
-
lambda:
|
|
1396
|
-
|
|
1397
|
-
|
|
1396
|
+
lambda: (
|
|
1397
|
+
self.host.view_3d_manager.plotter.reset_camera()
|
|
1398
|
+
if self.host.view_3d_manager.plotter
|
|
1399
|
+
else None
|
|
1400
|
+
)
|
|
1398
1401
|
)
|
|
1399
1402
|
reset_3d_view_action.setShortcut(QKeySequence("Ctrl+Shift+R"))
|
|
1400
1403
|
view_menu.addAction(reset_3d_view_action)
|
|
@@ -1750,8 +1753,9 @@ class MainInitManager:
|
|
|
1750
1753
|
if key.endswith("_OBABEL") and not OBABEL_AVAILABLE:
|
|
1751
1754
|
action.setEnabled(False)
|
|
1752
1755
|
action.triggered.connect(
|
|
1753
|
-
lambda checked,
|
|
1754
|
-
|
|
1756
|
+
lambda checked, m=key: (
|
|
1757
|
+
self.host.compute_manager.set_optimization_method(m)
|
|
1758
|
+
)
|
|
1755
1759
|
)
|
|
1756
1760
|
self.optimization_menu.addAction(action)
|
|
1757
1761
|
self.opt_group.addAction(action)
|
|
@@ -1796,8 +1800,9 @@ class MainInitManager:
|
|
|
1796
1800
|
action = QAction(label, self.host)
|
|
1797
1801
|
action.setCheckable(True)
|
|
1798
1802
|
action.triggered.connect(
|
|
1799
|
-
lambda checked,
|
|
1800
|
-
|
|
1803
|
+
lambda checked, m=key_upper: (
|
|
1804
|
+
self.host.compute_manager.set_optimization_method(m)
|
|
1805
|
+
)
|
|
1801
1806
|
)
|
|
1802
1807
|
|
|
1803
1808
|
self.optimization_menu.insertAction(self.opt3d_separator, action)
|