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.
Files changed (86) hide show
  1. {moleditpy-4.10.2 → moleditpy-4.10.3}/PKG-INFO +7 -3
  2. {moleditpy-4.10.2 → moleditpy-4.10.3}/README.md +6 -2
  3. {moleditpy-4.10.2 → moleditpy-4.10.3}/pyproject.toml +1 -1
  4. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/PKG-INFO +7 -3
  5. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
  6. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/main.py +3 -1
  7. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_manager.py +9 -5
  8. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_manager_window.py +50 -38
  9. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/compute_logic.py +9 -6
  10. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/io_logic.py +44 -11
  11. {moleditpy-4.10.2 → 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.2 → moleditpy-4.10.3}/src/moleditpy/ui/move_group_dialog.py +2 -209
  14. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/move_selected_atoms_dialog.py +17 -240
  15. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/plugin_menu_manager.py +7 -8
  16. {moleditpy-4.10.2 → moleditpy-4.10.3}/LICENSE +0 -0
  17. {moleditpy-4.10.2 → moleditpy-4.10.3}/setup.cfg +0 -0
  18. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  19. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  20. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/requires.txt +0 -0
  21. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/MoleditPy.egg-info/top_level.txt +0 -0
  22. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/__init__.py +0 -0
  23. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/__main__.py +0 -0
  24. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/assets/file_icon.ico +0 -0
  25. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/assets/icon.icns +0 -0
  26. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/assets/icon.ico +0 -0
  27. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/assets/icon.png +0 -0
  28. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/core/__init__.py +0 -0
  29. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/core/mol_geometry.py +0 -0
  30. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/core/molecular_data.py +0 -0
  31. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/plugins/__init__.py +0 -0
  32. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/plugins/plugin_interface.py +0 -0
  33. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/__init__.py +0 -0
  34. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/about_dialog.py +0 -0
  35. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/align_plane_dialog.py +0 -0
  36. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/alignment_dialog.py +0 -0
  37. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/analysis_window.py +0 -0
  38. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/angle_dialog.py +0 -0
  39. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/app_state.py +0 -0
  40. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/atom_item.py +0 -0
  41. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/atom_picking.py +0 -0
  42. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/base_picking_dialog.py +0 -0
  43. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/bond_item.py +0 -0
  44. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/bond_length_dialog.py +0 -0
  45. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/calculation_worker.py +0 -0
  46. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/chain_mixin.py +0 -0
  47. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/color_settings_dialog.py +0 -0
  48. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/constrained_optimization_dialog.py +0 -0
  49. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/custom_interactor_style.py +0 -0
  50. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
  51. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
  52. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/dialog_logic.py +0 -0
  53. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/dihedral_dialog.py +0 -0
  54. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/edit_3d_logic.py +0 -0
  55. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/edit_actions_logic.py +0 -0
  56. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/export_logic.py +0 -0
  57. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/geometry_base_dialog.py +0 -0
  58. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/main_window.py +0 -0
  59. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/mirror_dialog.py +0 -0
  60. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/molecular_scene_handler.py +0 -0
  61. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/molecule_scene.py +0 -0
  62. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  63. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/planarize_dialog.py +0 -0
  64. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/preview_molecule.py +0 -0
  65. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_dialog.py +0 -0
  66. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
  67. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
  68. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
  69. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
  70. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
  71. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
  72. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/string_importers.py +0 -0
  73. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/template_preview_item.py +0 -0
  74. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/template_preview_view.py +0 -0
  75. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/translation_dialog.py +0 -0
  76. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/ui_manager.py +0 -0
  77. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/user_template_dialog.py +0 -0
  78. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/view_3d_logic.py +0 -0
  79. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/ui/zoomable_view.py +0 -0
  80. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/__init__.py +0 -0
  81. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/constants.py +0 -0
  82. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/default_settings.py +0 -0
  83. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/hit_radius.py +0 -0
  84. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
  85. {moleditpy-4.10.2 → moleditpy-4.10.3}/src/moleditpy/utils/suppress_log.py +0 -0
  86. {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.2
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.2"
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.2
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": "",
@@ -115,7 +115,8 @@ class PluginManagerWindow(QDialog):
115
115
  btn_layout = QHBoxLayout()
116
116
 
117
117
  btn_reload = QPushButton("Reload Plugins")
118
- 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())
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
- ) -> tuple[QWidget, QCheckBox]:
148
- """Create a centered QCheckBox container widget without custom stylesheets."""
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, checkbox
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
- container, _ = self._create_checkbox_container(is_enabled)
169
- self.table.setCellWidget(row, 0, container)
169
+ self.table.setCellWidget(
170
+ row, 0, self._create_checkbox_container(is_enabled)
171
+ )
170
172
 
171
- # Store the plugin index in the item's UserRole for stable reference during filtering
172
- enabled_item = QTableWidgetItem()
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
- item = self.table.item(row, 0)
272
- plugin_idx = item.data(Qt.ItemDataRole.UserRole) if item else row
273
- if plugin_idx is not None and 0 <= plugin_idx < len(
274
- self.plugin_manager.plugins
275
- ):
276
- plugin = self.plugin_manager.plugins[plugin_idx]
277
- status_checkbox = self._status_checkbox(row)
278
- if status_checkbox is not None and not status_checkbox.isChecked():
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
- 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
+ """
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 for a table row, accounting for search filtering."""
328
- if row < 0 or row >= self.table.rowCount():
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
- item = self.table.item(row, 0)
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
- info = self.plugin_manager.get_plugin_info_safe(init_path)
460
- 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}"
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
- """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
 
@@ -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
- for encoding in ("utf-8-sig", "euc_jp", "cp932", "shift_jis"):
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
- return f.readlines()
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
- fixed_block = self._load_mol_block_text(file_path)
843
- # MolFromMolBlock can come back empty on a malformed file; the
844
- # stub does not say so, hence the explicit declaration. The None
845
- # check below covers that.
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
- fixed_block = self._load_mol_block_text(file_path)
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: (self.host.view_3d_manager.set_3d_style(k))
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: self.host.view_3d_manager.plotter.reset_camera()
1396
- if self.host.view_3d_manager.plotter
1397
- else None
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
- m=key: self.host.compute_manager.set_optimization_method(m)
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
- m=key_upper: self.host.compute_manager.set_optimization_method(m)
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)