MoleditPy 4.10.1__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.1 → moleditpy-4.10.3}/PKG-INFO +7 -3
- {moleditpy-4.10.1 → moleditpy-4.10.3}/README.md +6 -2
- {moleditpy-4.10.1 → moleditpy-4.10.3}/pyproject.toml +1 -1
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/PKG-INFO +7 -3
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/main.py +3 -1
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_manager.py +9 -5
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_manager_window.py +188 -66
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/compute_logic.py +9 -6
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/io_logic.py +44 -11
- {moleditpy-4.10.1 → 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.1 → moleditpy-4.10.3}/src/moleditpy/ui/move_group_dialog.py +2 -209
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/move_selected_atoms_dialog.py +17 -240
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/plugin_menu_manager.py +7 -8
- {moleditpy-4.10.1 → moleditpy-4.10.3}/LICENSE +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/setup.cfg +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/requires.txt +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/__init__.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/__main__.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/core/__init__.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/core/mol_geometry.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/core/molecular_data.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/plugins/__init__.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_interface.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/__init__.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/about_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/align_plane_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/alignment_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/analysis_window.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/angle_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/app_state.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/atom_item.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/atom_picking.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/base_picking_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/bond_item.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/bond_length_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/calculation_worker.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/chain_mixin.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/color_settings_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/custom_interactor_style.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/dialog_logic.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/dihedral_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/edit_3d_logic.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/edit_actions_logic.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/export_logic.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/geometry_base_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/main_window.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/mirror_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/molecular_scene_handler.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/molecule_scene.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/planarize_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/preview_molecule.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/string_importers.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/template_preview_item.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/template_preview_view.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/translation_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/ui_manager.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/user_template_dialog.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/view_3d_logic.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/zoomable_view.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/__init__.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/constants.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/default_settings.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/hit_radius.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/suppress_log.py +0 -0
- {moleditpy-4.10.1 → 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": "",
|
|
@@ -13,7 +13,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
13
13
|
import logging
|
|
14
14
|
import os
|
|
15
15
|
import shutil
|
|
16
|
-
from typing import Any, Optional
|
|
16
|
+
from typing import Any, Optional, cast
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
from PyQt6.QtCore import Qt, QUrl
|
|
@@ -25,6 +25,7 @@ from PyQt6.QtWidgets import (
|
|
|
25
25
|
QHBoxLayout,
|
|
26
26
|
QHeaderView,
|
|
27
27
|
QLabel,
|
|
28
|
+
QLineEdit,
|
|
28
29
|
QMessageBox,
|
|
29
30
|
QPushButton,
|
|
30
31
|
QTableWidget,
|
|
@@ -42,42 +43,68 @@ class PluginManagerWindow(QDialog):
|
|
|
42
43
|
super().__init__(parent)
|
|
43
44
|
self.btn_remove: Any = None
|
|
44
45
|
self.table: Any = None
|
|
46
|
+
self.search_input: Any = None
|
|
45
47
|
self.plugin_manager = plugin_manager
|
|
46
48
|
self._preferences_applied = False
|
|
47
49
|
self.setWindowTitle("Plugin Manager")
|
|
48
|
-
self.resize(
|
|
50
|
+
self.resize(850, 520)
|
|
49
51
|
self.setAcceptDrops(True) # Enable drag & drop for the whole window
|
|
50
52
|
|
|
51
53
|
self.init_ui()
|
|
52
54
|
self.refresh_plugin_list()
|
|
53
55
|
|
|
54
56
|
def init_ui(self) -> None:
|
|
55
|
-
"""Build
|
|
57
|
+
"""Build plugin manager UI with table, buttons, search box, and drop support."""
|
|
56
58
|
layout = QVBoxLayout(self)
|
|
57
59
|
|
|
58
60
|
lbl_info = QLabel(
|
|
59
|
-
"Drag & Drop .py or .zip files
|
|
61
|
+
"Drag & Drop .py or .zip files to install plugins. Use checkboxes to enable/disable."
|
|
60
62
|
)
|
|
61
63
|
lbl_info.setStyleSheet("color: gray; font-style: italic;")
|
|
62
64
|
layout.addWidget(lbl_info)
|
|
63
65
|
|
|
66
|
+
# Search Box
|
|
67
|
+
search_layout = QHBoxLayout()
|
|
68
|
+
search_label = QLabel("Search:")
|
|
69
|
+
self.search_input = QLineEdit()
|
|
70
|
+
self.search_input.setPlaceholderText(
|
|
71
|
+
"Search plugins by name, author, location, or description..."
|
|
72
|
+
)
|
|
73
|
+
self.search_input.setClearButtonEnabled(True)
|
|
74
|
+
self.search_input.textChanged.connect(self.filter_plugins)
|
|
75
|
+
search_layout.addWidget(search_label)
|
|
76
|
+
search_layout.addWidget(self.search_input)
|
|
77
|
+
layout.addLayout(search_layout)
|
|
78
|
+
|
|
64
79
|
# Plugin Table
|
|
65
80
|
self.table = QTableWidget()
|
|
66
|
-
self.table.setColumnCount(
|
|
81
|
+
self.table.setColumnCount(7)
|
|
67
82
|
self.table.setHorizontalHeaderLabels(
|
|
68
|
-
[
|
|
83
|
+
[
|
|
84
|
+
"Enabled",
|
|
85
|
+
"Status",
|
|
86
|
+
"Name",
|
|
87
|
+
"Version",
|
|
88
|
+
"Author",
|
|
89
|
+
"Location",
|
|
90
|
+
"Description",
|
|
91
|
+
]
|
|
69
92
|
)
|
|
70
93
|
self.table.horizontalHeader().setSectionResizeMode(
|
|
71
94
|
QHeaderView.ResizeMode.Interactive
|
|
72
95
|
)
|
|
73
96
|
self.table.horizontalHeader().setSectionResizeMode(
|
|
74
|
-
|
|
97
|
+
0, QHeaderView.ResizeMode.ResizeToContents
|
|
98
|
+
)
|
|
99
|
+
self.table.horizontalHeader().setSectionResizeMode(
|
|
100
|
+
5, QHeaderView.ResizeMode.Interactive
|
|
75
101
|
)
|
|
76
102
|
self.table.horizontalHeader().setSectionResizeMode(
|
|
77
|
-
|
|
103
|
+
6, QHeaderView.ResizeMode.Stretch
|
|
78
104
|
) # Description stretches
|
|
79
|
-
self.table.setColumnWidth(
|
|
80
|
-
self.table.setColumnWidth(
|
|
105
|
+
self.table.setColumnWidth(0, 70) # Enabled checkbox
|
|
106
|
+
self.table.setColumnWidth(1, 100) # Status
|
|
107
|
+
self.table.setColumnWidth(2, 180) # Name
|
|
81
108
|
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
82
109
|
self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
83
110
|
self.table.itemSelectionChanged.connect(self.update_button_state)
|
|
@@ -88,7 +115,8 @@ class PluginManagerWindow(QDialog):
|
|
|
88
115
|
btn_layout = QHBoxLayout()
|
|
89
116
|
|
|
90
117
|
btn_reload = QPushButton("Reload Plugins")
|
|
91
|
-
|
|
118
|
+
# Not connect(self.on_reload): clicked(bool) would land in *silent*.
|
|
119
|
+
btn_reload.clicked.connect(lambda: self.on_reload())
|
|
92
120
|
btn_layout.addWidget(btn_reload)
|
|
93
121
|
|
|
94
122
|
btn_folder = QPushButton("Open Plugin Folder")
|
|
@@ -115,6 +143,20 @@ class PluginManagerWindow(QDialog):
|
|
|
115
143
|
|
|
116
144
|
layout.addLayout(btn_layout)
|
|
117
145
|
|
|
146
|
+
def _create_checkbox_container(
|
|
147
|
+
self, checked: bool, tooltip: str = "Enable or disable this plugin"
|
|
148
|
+
) -> QWidget:
|
|
149
|
+
"""Create a centered QCheckBox container; read it back via _status_checkbox."""
|
|
150
|
+
container = QWidget()
|
|
151
|
+
box_layout = QHBoxLayout(container)
|
|
152
|
+
box_layout.setContentsMargins(0, 0, 0, 0)
|
|
153
|
+
box_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
154
|
+
checkbox = QCheckBox()
|
|
155
|
+
checkbox.setChecked(checked)
|
|
156
|
+
checkbox.setToolTip(tooltip)
|
|
157
|
+
box_layout.addWidget(checkbox)
|
|
158
|
+
return container
|
|
159
|
+
|
|
118
160
|
def refresh_plugin_list(self) -> None:
|
|
119
161
|
"""Repopulate the plugin table from the current plugin registry."""
|
|
120
162
|
self.table.setRowCount(0)
|
|
@@ -122,22 +164,35 @@ class PluginManagerWindow(QDialog):
|
|
|
122
164
|
|
|
123
165
|
self.table.setRowCount(len(plugins))
|
|
124
166
|
for row, p in enumerate(plugins):
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
self.table.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
167
|
+
# Column 0: Enabled Checkbox (Clean native check, centered)
|
|
168
|
+
is_enabled = not p.get("disabled", False)
|
|
169
|
+
self.table.setCellWidget(
|
|
170
|
+
row, 0, self._create_checkbox_container(is_enabled)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Empty item behind the widget so the column still selects the row.
|
|
174
|
+
self.table.setItem(row, 0, QTableWidgetItem())
|
|
175
|
+
|
|
176
|
+
# Column 1: Status (colored text)
|
|
177
|
+
status = str(p.get("status", "Unknown"))
|
|
178
|
+
status_item = QTableWidgetItem(status)
|
|
179
|
+
status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
180
|
+
if status.startswith("Error"):
|
|
181
|
+
status_item.setForeground(Qt.GlobalColor.red)
|
|
182
|
+
elif status == "Loaded":
|
|
183
|
+
status_item.setForeground(Qt.GlobalColor.darkGreen)
|
|
184
|
+
elif status in ("No Entry Point", "Disabled"):
|
|
185
|
+
status_item.setForeground(Qt.GlobalColor.gray)
|
|
186
|
+
self.table.setItem(row, 1, status_item)
|
|
187
|
+
|
|
188
|
+
# Column 2: Name
|
|
189
|
+
self.table.setItem(row, 2, QTableWidgetItem(str(p.get("name", "Unknown"))))
|
|
190
|
+
# Column 3: Version
|
|
191
|
+
self.table.setItem(row, 3, QTableWidgetItem(str(p.get("version", ""))))
|
|
192
|
+
# Column 4: Author
|
|
193
|
+
self.table.setItem(row, 4, QTableWidgetItem(str(p.get("author", ""))))
|
|
194
|
+
|
|
195
|
+
# Column 5: Location (Relative Path)
|
|
141
196
|
full_path = p.get("filepath", "")
|
|
142
197
|
rel_path = ""
|
|
143
198
|
if full_path:
|
|
@@ -147,38 +202,85 @@ class PluginManagerWindow(QDialog):
|
|
|
147
202
|
)
|
|
148
203
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
149
204
|
rel_path = os.path.basename(full_path)
|
|
150
|
-
self.table.setItem(row,
|
|
205
|
+
self.table.setItem(row, 5, QTableWidgetItem(str(rel_path)))
|
|
206
|
+
|
|
207
|
+
# Column 6: Description
|
|
208
|
+
self.table.setItem(row, 6, QTableWidgetItem(str(p.get("description", ""))))
|
|
209
|
+
|
|
210
|
+
if self.search_input and self.search_input.text():
|
|
211
|
+
self.filter_plugins(self.search_input.text())
|
|
212
|
+
|
|
213
|
+
def filter_plugins(self, text: str) -> None:
|
|
214
|
+
"""Filter table rows based on search text."""
|
|
215
|
+
query = text.strip().lower()
|
|
216
|
+
for row in range(self.table.rowCount()):
|
|
217
|
+
if not query:
|
|
218
|
+
self.table.setRowHidden(row, False)
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
# Check status, name, author, location, and description
|
|
222
|
+
status = (
|
|
223
|
+
self.table.item(row, 1).text().lower()
|
|
224
|
+
if self.table.item(row, 1)
|
|
225
|
+
else ""
|
|
226
|
+
)
|
|
227
|
+
name = (
|
|
228
|
+
self.table.item(row, 2).text().lower()
|
|
229
|
+
if self.table.item(row, 2)
|
|
230
|
+
else ""
|
|
231
|
+
)
|
|
232
|
+
author = (
|
|
233
|
+
self.table.item(row, 4).text().lower()
|
|
234
|
+
if self.table.item(row, 4)
|
|
235
|
+
else ""
|
|
236
|
+
)
|
|
237
|
+
location = (
|
|
238
|
+
self.table.item(row, 5).text().lower()
|
|
239
|
+
if self.table.item(row, 5)
|
|
240
|
+
else ""
|
|
241
|
+
)
|
|
242
|
+
description = (
|
|
243
|
+
self.table.item(row, 6).text().lower()
|
|
244
|
+
if self.table.item(row, 6)
|
|
245
|
+
else ""
|
|
246
|
+
)
|
|
151
247
|
|
|
152
|
-
|
|
248
|
+
match = (
|
|
249
|
+
query in name
|
|
250
|
+
or query in author
|
|
251
|
+
or query in location
|
|
252
|
+
or query in description
|
|
253
|
+
or query in status
|
|
254
|
+
)
|
|
255
|
+
self.table.setRowHidden(row, not match)
|
|
153
256
|
|
|
154
|
-
|
|
155
|
-
status = str(p.get("status", ""))
|
|
156
|
-
color = None
|
|
157
|
-
checkbox_color = None
|
|
158
|
-
if status.startswith("Error"):
|
|
159
|
-
color = Qt.GlobalColor.red
|
|
160
|
-
checkbox_color = "red"
|
|
161
|
-
elif status == "Loaded":
|
|
162
|
-
color = Qt.GlobalColor.darkGreen
|
|
163
|
-
checkbox_color = "darkgreen"
|
|
164
|
-
elif status in ("No Entry Point", "Disabled"):
|
|
165
|
-
color = Qt.GlobalColor.gray
|
|
166
|
-
checkbox_color = "gray"
|
|
257
|
+
self._drop_hidden_selection()
|
|
167
258
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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()
|
|
172
266
|
|
|
173
267
|
def _status_checkbox(self, row: int) -> Optional[QCheckBox]:
|
|
174
268
|
"""Return the visible status checkbox for a plugin table row."""
|
|
175
|
-
|
|
176
|
-
|
|
269
|
+
container = self.table.cellWidget(row, 0)
|
|
270
|
+
if isinstance(container, QCheckBox):
|
|
271
|
+
return container
|
|
272
|
+
if isinstance(container, QWidget):
|
|
273
|
+
checkbox = container.findChild(QCheckBox)
|
|
274
|
+
return checkbox
|
|
275
|
+
return None
|
|
177
276
|
|
|
178
277
|
def _save_checkbox_preferences(self) -> set[str]:
|
|
179
|
-
"""Collect and persist checkbox choices."""
|
|
278
|
+
"""Collect and persist checkbox choices, hidden rows included."""
|
|
180
279
|
disabled_paths = set()
|
|
181
|
-
for row
|
|
280
|
+
for row in range(self.table.rowCount()):
|
|
281
|
+
plugin = self._plugin_for_row(row)
|
|
282
|
+
if plugin is None:
|
|
283
|
+
continue
|
|
182
284
|
status_checkbox = self._status_checkbox(row)
|
|
183
285
|
if status_checkbox is not None and not status_checkbox.isChecked():
|
|
184
286
|
filepath = plugin.get("filepath")
|
|
@@ -202,9 +304,12 @@ class PluginManagerWindow(QDialog):
|
|
|
202
304
|
self.plugin_manager.discover_plugins()
|
|
203
305
|
|
|
204
306
|
def done(self, result: int) -> None:
|
|
205
|
-
"""Apply plugin preferences for every way the dialog can close.
|
|
206
|
-
|
|
307
|
+
"""Apply plugin preferences for every way the dialog can close.
|
|
308
|
+
|
|
309
|
+
Close first: applying them re-runs every plugin's ``initialize()``.
|
|
310
|
+
"""
|
|
207
311
|
super().done(result)
|
|
312
|
+
self._apply_plugin_preferences()
|
|
208
313
|
|
|
209
314
|
def update_button_state(self) -> None:
|
|
210
315
|
"""Enable or disable the Remove button based on table selection."""
|
|
@@ -226,6 +331,17 @@ class PluginManagerWindow(QDialog):
|
|
|
226
331
|
self.plugin_manager.discover_plugins()
|
|
227
332
|
self.refresh_plugin_list()
|
|
228
333
|
|
|
334
|
+
def _plugin_for_row(self, row: int) -> Optional[dict[str, Any]]:
|
|
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
|
+
):
|
|
342
|
+
return None
|
|
343
|
+
return cast(dict[str, Any], self.plugin_manager.plugins[row])
|
|
344
|
+
|
|
229
345
|
def on_remove_plugin(self) -> None:
|
|
230
346
|
"""Delete the selected plugin file or folder and reload."""
|
|
231
347
|
row = self.table.currentRow()
|
|
@@ -233,9 +349,8 @@ class PluginManagerWindow(QDialog):
|
|
|
233
349
|
QMessageBox.warning(self, "Warning", "Please select a plugin to remove.")
|
|
234
350
|
return
|
|
235
351
|
|
|
236
|
-
|
|
237
|
-
if
|
|
238
|
-
plugin = self.plugin_manager.plugins[row]
|
|
352
|
+
plugin = self._plugin_for_row(row)
|
|
353
|
+
if plugin is not None:
|
|
239
354
|
filepath = plugin.get("filepath")
|
|
240
355
|
|
|
241
356
|
if filepath and os.path.exists(filepath):
|
|
@@ -280,15 +395,15 @@ class PluginManagerWindow(QDialog):
|
|
|
280
395
|
def show_plugin_details(self, item: QTableWidgetItem) -> None:
|
|
281
396
|
"""Show a message box with full metadata for the double-clicked plugin."""
|
|
282
397
|
row = item.row()
|
|
283
|
-
|
|
284
|
-
|
|
398
|
+
plugin = self._plugin_for_row(row)
|
|
399
|
+
if plugin is not None:
|
|
285
400
|
msg = (
|
|
286
|
-
f"Name: {
|
|
287
|
-
f"Version: {
|
|
288
|
-
f"Author: {
|
|
289
|
-
f"Status: {
|
|
290
|
-
f"Location: {
|
|
291
|
-
f"Description:\n{
|
|
401
|
+
f"Name: {plugin.get('name', 'Unknown')}\n"
|
|
402
|
+
f"Version: {plugin.get('version', 'Unknown')}\n"
|
|
403
|
+
f"Author: {plugin.get('author', 'Unknown')}\n"
|
|
404
|
+
f"Status: {plugin.get('status', 'Unknown')}\n"
|
|
405
|
+
f"Location: {plugin.get('filepath', 'Unknown')}\n\n"
|
|
406
|
+
f"Description:\n{plugin.get('description', 'No description available.')}"
|
|
292
407
|
)
|
|
293
408
|
QMessageBox.information(self, "Plugin Details", msg)
|
|
294
409
|
|
|
@@ -316,7 +431,8 @@ class PluginManagerWindow(QDialog):
|
|
|
316
431
|
is_folder = False
|
|
317
432
|
|
|
318
433
|
if os.path.isfile(file_path):
|
|
319
|
-
# Special handling: If user drops __init__.py,
|
|
434
|
+
# Special handling: If user drops __init__.py,
|
|
435
|
+
# assume they want to install the package (folder)
|
|
320
436
|
if os.path.basename(file_path) == "__init__.py":
|
|
321
437
|
file_path = os.path.dirname(file_path)
|
|
322
438
|
is_valid = True
|
|
@@ -346,8 +462,14 @@ class PluginManagerWindow(QDialog):
|
|
|
346
462
|
# Try to parse __init__.py if it exists
|
|
347
463
|
init_path = os.path.join(file_path, "__init__.py")
|
|
348
464
|
if os.path.exists(init_path):
|
|
349
|
-
|
|
350
|
-
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}"
|
|
351
473
|
|
|
352
474
|
elif is_zip:
|
|
353
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
|
|