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.
Files changed (86) hide show
  1. {moleditpy-4.10.1 → moleditpy-4.10.3}/PKG-INFO +7 -3
  2. {moleditpy-4.10.1 → moleditpy-4.10.3}/README.md +6 -2
  3. {moleditpy-4.10.1 → moleditpy-4.10.3}/pyproject.toml +1 -1
  4. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/PKG-INFO +7 -3
  5. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
  6. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/main.py +3 -1
  7. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_manager.py +9 -5
  8. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_manager_window.py +188 -66
  9. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/compute_logic.py +9 -6
  10. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/io_logic.py +44 -11
  11. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/main_window_init.py +15 -10
  12. moleditpy-4.10.3/src/moleditpy/ui/move_dialog_mixin.py +250 -0
  13. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/move_group_dialog.py +2 -209
  14. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/move_selected_atoms_dialog.py +17 -240
  15. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/plugin_menu_manager.py +7 -8
  16. {moleditpy-4.10.1 → moleditpy-4.10.3}/LICENSE +0 -0
  17. {moleditpy-4.10.1 → moleditpy-4.10.3}/setup.cfg +0 -0
  18. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  19. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  20. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/requires.txt +0 -0
  21. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/MoleditPy.egg-info/top_level.txt +0 -0
  22. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/__init__.py +0 -0
  23. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/__main__.py +0 -0
  24. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/assets/file_icon.ico +0 -0
  25. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/assets/icon.icns +0 -0
  26. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/assets/icon.ico +0 -0
  27. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/assets/icon.png +0 -0
  28. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/core/__init__.py +0 -0
  29. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/core/mol_geometry.py +0 -0
  30. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/core/molecular_data.py +0 -0
  31. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/plugins/__init__.py +0 -0
  32. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_interface.py +0 -0
  33. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/__init__.py +0 -0
  34. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/about_dialog.py +0 -0
  35. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/align_plane_dialog.py +0 -0
  36. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/alignment_dialog.py +0 -0
  37. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/analysis_window.py +0 -0
  38. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/angle_dialog.py +0 -0
  39. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/app_state.py +0 -0
  40. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/atom_item.py +0 -0
  41. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/atom_picking.py +0 -0
  42. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/base_picking_dialog.py +0 -0
  43. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/bond_item.py +0 -0
  44. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/bond_length_dialog.py +0 -0
  45. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/calculation_worker.py +0 -0
  46. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/chain_mixin.py +0 -0
  47. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/color_settings_dialog.py +0 -0
  48. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/constrained_optimization_dialog.py +0 -0
  49. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/custom_interactor_style.py +0 -0
  50. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
  51. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
  52. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/dialog_logic.py +0 -0
  53. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/dihedral_dialog.py +0 -0
  54. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/edit_3d_logic.py +0 -0
  55. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/edit_actions_logic.py +0 -0
  56. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/export_logic.py +0 -0
  57. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/geometry_base_dialog.py +0 -0
  58. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/main_window.py +0 -0
  59. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/mirror_dialog.py +0 -0
  60. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/molecular_scene_handler.py +0 -0
  61. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/molecule_scene.py +0 -0
  62. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  63. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/planarize_dialog.py +0 -0
  64. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/preview_molecule.py +0 -0
  65. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_dialog.py +0 -0
  66. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
  67. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
  68. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
  69. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
  70. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
  71. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
  72. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/string_importers.py +0 -0
  73. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/template_preview_item.py +0 -0
  74. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/template_preview_view.py +0 -0
  75. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/translation_dialog.py +0 -0
  76. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/ui_manager.py +0 -0
  77. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/user_template_dialog.py +0 -0
  78. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/view_3d_logic.py +0 -0
  79. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/ui/zoomable_view.py +0 -0
  80. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/__init__.py +0 -0
  81. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/constants.py +0 -0
  82. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/default_settings.py +0 -0
  83. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/hit_radius.py +0 -0
  84. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
  85. {moleditpy-4.10.1 → moleditpy-4.10.3}/src/moleditpy/utils/suppress_log.py +0 -0
  86. {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.1
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
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17268532.svg)](https://doi.org/10.5281/zenodo.17268532)
708
+ ![MoleditPy](https://img.shields.io/badge/MoleditPy-v4-3577F7)
708
709
  [![Powered by RDKit](https://img.shields.io/badge/Powered%20by-RDKit-3838ff.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEXc3NwUFP8UPP9kZP+MjP+0tP////9ZXZotAAAAAXRSTlMAQObYZgAAAAFiS0dEBmFmuH0AAAAHdElNRQfmAwsPGi+MyC9RAAAAQElEQVQI12NgQABGQUEBMENISUkRLKBsbGwEEhIyBgJFsICLC0iIUdnExcUZwnANQWfApKCK4doRBsKtQFgKAQC5Ww1JEHSEkAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMy0xMVQxNToyNjo0NyswMDowMDzr2J4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDMtMTFUMTU6MjY6NDcrMDA6MDBNtmAiAAAAAElFTkSuQmCC)](https://www.rdkit.org/)
709
710
  [![PyPI version](https://badge.fury.io/py/MoleditPy.svg)](https://badge.fury.io/py/MoleditPy)
710
- [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
+ [![Python Versions](https://img.shields.io/badge/python-3.9--3.14-blue.svg)](https://pypi.org/project/MoleditPy/)
711
712
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
713
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
713
714
  [![codecov](https://codecov.io/gh/HiroYokoyama/python_molecular_editor/graph/badge.svg)](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
- ![](img/icon.png)
731
+ <p align="center">
732
+ <img src="img/icon.png" width="500" alt="MoleditPy Icon">
733
+ </p>
734
+
731
735
  ![](img/screenshot.png)
732
736
 
733
737
 
@@ -1,9 +1,10 @@
1
1
  # MoleditPy — A Python Molecular Editor
2
2
 
3
3
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17268532.svg)](https://doi.org/10.5281/zenodo.17268532)
4
+ ![MoleditPy](https://img.shields.io/badge/MoleditPy-v4-3577F7)
4
5
  [![Powered by RDKit](https://img.shields.io/badge/Powered%20by-RDKit-3838ff.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEXc3NwUFP8UPP9kZP+MjP+0tP////9ZXZotAAAAAXRSTlMAQObYZgAAAAFiS0dEBmFmuH0AAAAHdElNRQfmAwsPGi+MyC9RAAAAQElEQVQI12NgQABGQUEBMENISUkRLKBsbGwEEhIyBgJFsICLC0iIUdnExcUZwnANQWfApKCK4doRBsKtQFgKAQC5Ww1JEHSEkAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMy0xMVQxNToyNjo0NyswMDowMDzr2J4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDMtMTFUMTU6MjY6NDcrMDA6MDBNtmAiAAAAAElFTkSuQmCC)](https://www.rdkit.org/)
5
6
  [![PyPI version](https://badge.fury.io/py/MoleditPy.svg)](https://badge.fury.io/py/MoleditPy)
6
- [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
7
+ [![Python Versions](https://img.shields.io/badge/python-3.9--3.14-blue.svg)](https://pypi.org/project/MoleditPy/)
7
8
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
8
9
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
9
10
  [![codecov](https://codecov.io/gh/HiroYokoyama/python_molecular_editor/graph/badge.svg)](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
- ![](img/icon.png)
27
+ <p align="center">
28
+ <img src="img/icon.png" width="500" alt="MoleditPy Icon">
29
+ </p>
30
+
27
31
  ![](img/screenshot.png)
28
32
 
29
33
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "4.10.1"
8
+ version = "4.10.3"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 4.10.1
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
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17268532.svg)](https://doi.org/10.5281/zenodo.17268532)
708
+ ![MoleditPy](https://img.shields.io/badge/MoleditPy-v4-3577F7)
708
709
  [![Powered by RDKit](https://img.shields.io/badge/Powered%20by-RDKit-3838ff.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEXc3NwUFP8UPP9kZP+MjP+0tP////9ZXZotAAAAAXRSTlMAQObYZgAAAAFiS0dEBmFmuH0AAAAHdElNRQfmAwsPGi+MyC9RAAAAQElEQVQI12NgQABGQUEBMENISUkRLKBsbGwEEhIyBgJFsICLC0iIUdnExcUZwnANQWfApKCK4doRBsKtQFgKAQC5Ww1JEHSEkAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMy0xMVQxNToyNjo0NyswMDowMDzr2J4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDMtMTFUMTU6MjY6NDcrMDA6MDBNtmAiAAAAAElFTkSuQmCC)](https://www.rdkit.org/)
709
710
  [![PyPI version](https://badge.fury.io/py/MoleditPy.svg)](https://badge.fury.io/py/MoleditPy)
710
- [![Python Versions](https://img.shields.io/pypi/pyversions/MoleditPy.svg)](https://pypi.org/project/MoleditPy/)
711
+ [![Python Versions](https://img.shields.io/badge/python-3.9--3.14-blue.svg)](https://pypi.org/project/MoleditPy/)
711
712
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
712
713
  [![Build Status](https://github.com/HiroYokoyama/python_molecular_editor/actions/workflows/tests.yml/badge.svg)](https://github.com/HiroYokoyama/python_molecular_editor/actions)
713
714
  [![codecov](https://codecov.io/gh/HiroYokoyama/python_molecular_editor/graph/badge.svg)](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
- ![](img/icon.png)
731
+ <p align="center">
732
+ <img src="img/icon.png" width="500" alt="MoleditPy Icon">
733
+ </p>
734
+
731
735
  ![](img/screenshot.png)
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(metadata_file)
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
- info = self.get_plugin_info_safe(filepath)
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.get("name", module_name),
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(self, file_path: str) -> Dict[str, str]:
804
- """Extracts plugin metadata using AST parsing (safe, no execution)."""
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(800, 500)
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 the plugin manager UI with table, buttons, and drag-and-drop support."""
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 here to install plugins. Use the checkbox to enable/disable plugins."
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(6)
81
+ self.table.setColumnCount(7)
67
82
  self.table.setHorizontalHeaderLabels(
68
- ["Status", "Name", "Version", "Author", "Location", "Description"]
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
- 4, QHeaderView.ResizeMode.Interactive
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
- 5, QHeaderView.ResizeMode.Stretch
103
+ 6, QHeaderView.ResizeMode.Stretch
78
104
  ) # Description stretches
79
- self.table.setColumnWidth(1, 200) # Make Name column wider
80
- self.table.setColumnWidth(0, 120) # Keep the status checkbox on one line
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
- btn_reload.clicked.connect(self.on_reload)
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
- # Keep an empty item for row selection; the checkbox owns visible status text.
126
- status_item = QTableWidgetItem()
127
- self.table.setItem(row, 0, status_item)
128
-
129
- # Use an explicit widget so macOS keeps the checkbox and status text
130
- # in a stable horizontal layout instead of stacking the item indicator.
131
- status_checkbox = QCheckBox(str(p.get("status", "Unknown")))
132
- status_checkbox.setChecked(not p.get("disabled", False))
133
- status_checkbox.setToolTip("Enable or disable this plugin")
134
- status_checkbox.setStyleSheet("QCheckBox { padding-left: 4px; }")
135
- self.table.setCellWidget(row, 0, status_checkbox)
136
- self.table.setItem(row, 1, QTableWidgetItem(str(p.get("name", "Unknown"))))
137
- self.table.setItem(row, 2, QTableWidgetItem(str(p.get("version", ""))))
138
- self.table.setItem(row, 3, QTableWidgetItem(str(p.get("author", ""))))
139
-
140
- # Location (Relative Path)
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, 4, QTableWidgetItem(str(rel_path)))
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
- self.table.setItem(row, 5, QTableWidgetItem(str(p.get("description", ""))))
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
- # Simple color coding for status
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
- if color:
169
- status_checkbox.setStyleSheet(
170
- f"QCheckBox {{ padding-left: 4px; color: {checkbox_color}; }}"
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
- checkbox = self.table.cellWidget(row, 0)
176
- return checkbox if isinstance(checkbox, QCheckBox) else None
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, plugin in enumerate(self.plugin_manager.plugins):
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
- self._apply_plugin_preferences()
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
- # Assuming table row index matches plugins list index (confirmed in refresh_plugin_list)
237
- if row < len(self.plugin_manager.plugins):
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
- if row < len(self.plugin_manager.plugins):
284
- p = self.plugin_manager.plugins[row]
398
+ plugin = self._plugin_for_row(row)
399
+ if plugin is not None:
285
400
  msg = (
286
- f"Name: {p.get('name', 'Unknown')}\n"
287
- f"Version: {p.get('version', 'Unknown')}\n"
288
- f"Author: {p.get('author', 'Unknown')}\n"
289
- f"Status: {p.get('status', 'Unknown')}\n"
290
- f"Location: {p.get('filepath', 'Unknown')}\n\n"
291
- f"Description:\n{p.get('description', 'No description available.')}"
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, assume they want to install the package (folder)
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
- info = self.plugin_manager.get_plugin_info_safe(init_path)
350
- info["description"] += f" (Package: {info['name']})"
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
- """Safely disconnect a signal, silently ignoring RuntimeError."""
69
+ """Disconnect a signal, tolerating one that has nothing connected."""
70
70
  try:
71
71
  signal.disconnect()
72
- except RuntimeError:
73
- # Safe defensive fallback catching RuntimeError
74
- logging.debug("Suppressed non-critical error", exc_info=True)
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
- self.host.init_manager.convert_button.clicked.connect(self.trigger_conversion) # type: ignore[union-attr]
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