MoleditPy-linux 4.2.4__tar.gz → 4.3.1__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 (81) hide show
  1. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/PKG-INFO +11 -8
  2. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/README.md +10 -7
  3. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/pyproject.toml +1 -1
  4. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/MoleditPy_linux.egg-info/PKG-INFO +11 -8
  5. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/main.py +123 -4
  6. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/plugins/plugin_manager.py +16 -7
  7. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/plugins/plugin_manager_window.py +2 -3
  8. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/align_plane_dialog.py +2 -3
  9. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/alignment_dialog.py +1 -2
  10. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/app_state.py +13 -5
  11. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/bond_item.py +2 -2
  12. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/compute_logic.py +6 -3
  13. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +36 -27
  14. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/custom_interactor_style.py +15 -13
  15. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +1 -1
  16. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/dialog_logic.py +2 -3
  17. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/edit_actions_logic.py +13 -1
  18. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/export_logic.py +2 -2
  19. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/io_logic.py +63 -20
  20. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/main_window.py +1 -1
  21. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/main_window_init.py +9 -6
  22. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/mirror_dialog.py +2 -4
  23. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/molecular_scene_handler.py +10 -7
  24. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/molecule_scene.py +3 -3
  25. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/move_group_dialog.py +1 -1
  26. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +1 -1
  27. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/planarize_dialog.py +2 -2
  28. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/plugin_menu_manager.py +57 -42
  29. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/template_preview_view.py +2 -2
  30. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/translation_dialog.py +1 -1
  31. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/ui_manager.py +6 -1
  32. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/user_template_dialog.py +8 -10
  33. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/view_3d_logic.py +17 -13
  34. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/LICENSE +0 -0
  35. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/setup.cfg +0 -0
  36. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
  37. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  38. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  39. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  40. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  41. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/__init__.py +0 -0
  42. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/__main__.py +0 -0
  43. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  44. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/assets/icon.icns +0 -0
  45. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/assets/icon.ico +0 -0
  46. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/assets/icon.png +0 -0
  47. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/core/__init__.py +0 -0
  48. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  49. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/core/molecular_data.py +0 -0
  50. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/plugins/__init__.py +0 -0
  51. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
  52. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/__init__.py +0 -0
  53. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/about_dialog.py +0 -0
  54. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  55. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
  56. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/atom_item.py +0 -0
  57. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/atom_picking.py +0 -0
  58. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  59. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
  60. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
  61. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  62. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  63. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
  64. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
  65. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  66. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
  67. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  68. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  69. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
  70. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
  71. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
  72. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
  73. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/string_importers.py +0 -0
  74. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  75. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  76. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/utils/__init__.py +0 -0
  77. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/utils/constants.py +0 -0
  78. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/utils/default_settings.py +0 -0
  79. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  80. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/utils/suppress_log.py +0 -0
  81. {moleditpy_linux-4.2.4 → moleditpy_linux-4.3.1}/src/moleditpy_linux/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.2.4
3
+ Version: 4.3.1
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
@@ -770,16 +770,17 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
770
770
  * **Stereochemistry Display:** Automatically identifies and displays R/S labels for chiral centers in the 3D view after conversion.
771
771
  * **File I/O:**
772
772
  * Save and load entire sessions, including 2D/3D data and constraints, with the native `.pmeprj` project file.
773
- * Import structures from **MOL/SDF** files or **SMILES** strings.
773
+ * Import structures from **MOL/SDF/XYZ** files or **SMILES/InChI** strings.
774
774
  * Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
775
- * Export 2D and 3D views as high-resolution PNG images.
775
+ * Export 2D views as high-resolution PNG or SVG images, and 3D views as PNG images.
776
+ * Export the 3D model as **STL** or **OBJ/MTL** files for 3D printing and rendering.
776
777
  * **Security note on `.pmeraw`:** the legacy `.pmeraw` project format uses Python pickle, which can execute arbitrary code when loaded. Only open `.pmeraw` files that you created yourself; prefer the JSON-based `.pmeprj` format for sharing.
777
778
 
778
779
  ### 4. Programmable & Extensible
779
780
 
780
781
  * **Python Plugin System:** Drop your Python scripts into the plugin folder, and they instantly become part of the application menu.
781
782
  * **Downloadable Plugins:** Explore and download specialized plugins from the [Plugin Explorer](https://hiroyokoyama.github.io/moleditpy-plugins/explorer/).
782
- * **Full API Access:** Plugins have direct access to the `MainWindow`, `RDKit` molecule objects, and `PyGraphics` items, allowing for limitless customization.
783
+ * **Full API Access:** Plugins have direct access to the `MainWindow`, `RDKit` molecule objects, and the Qt graphics items of the 2D scene, allowing for limitless customization.
783
784
  * **Rapid Prototyping:** Ideal for researchers who need to test new algorithms or workflow automations on the fly.
784
785
 
785
786
  ## Installation and Execution
@@ -835,7 +836,7 @@ moleditpy
835
836
  * **GUI and 2D Drawing (PyQt6):** The editor is built on a `QGraphicsScene`, where custom `AtomItem` and `BondItem` objects are interactively manipulated. The Undo/Redo feature is implemented by serializing the application state.
836
837
  * **Chemical Calculations (RDKit / Open Babel):** RDKit is used to generate molecule objects from 2D data, perform 3D coordinate generation, and calculate properties. Open Babel serves as a fallback for 3D conversion. All heavy computations are run on a separate `QThread` to keep the GUI responsive.
837
838
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
838
- * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
839
+ * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The `MainWindow` acts as a composition hub that delegates all work to specialized manager objects (state, I/O, 3D view, dialogs), ensuring long-term maintainability and easier verification.
839
840
 
840
841
  ## License & Disclaimer
841
842
 
@@ -905,16 +906,17 @@ Additionally, please cite the plugins you used.
905
906
  * **立体化学表示:** 3D変換後、キラル中心を自動的に認識し、R/Sラベルを3Dビューに表示します。
906
907
  * **ファイル入出力:**
907
908
  * 2D/3Dデータや制約情報を含むセッション全体を、独自のプロジェクトファイル (`.pmeprj`) として保存・読み込みできます。
908
- * **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
909
+ * **MOL/SDF/XYZ**ファイルや**SMILES/InChI**文字列から構造をインポートできます。
909
910
  * 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
910
- * 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
911
+ * 2Dビューを高解像度のPNG・SVG画像として、3DビューをPNG画像としてエクスポートできます。
912
+ * 3Dモデルを**STL**または**OBJ/MTL**ファイルとしてエクスポートでき、3Dプリントやレンダリングに利用できます。
911
913
  * **`.pmeraw` に関するセキュリティ上の注意:** 旧形式の `.pmeraw` プロジェクトは Python の pickle を使用しており、読み込み時に任意のコードが実行される可能性があります。自分で作成した `.pmeraw` ファイルのみを開き、共有には JSON ベースの `.pmeprj` 形式を使用してください。
912
914
 
913
915
  ### 4. プログラマブルで拡張可能
914
916
 
915
917
  * **Pythonプラグインシステム:** Pythonスクリプトをプラグインフォルダに入れるだけで、即座にアプリケーションメニューの一部として機能します。
916
918
  * **プラグインのダウンロード:** [Plugin Explorer](https://hiroyokoyama.github.io/moleditpy-plugins/explorer/) から特化したプラグインを探索・ダウンロードできます。
917
- * **フルAPIアクセス:** プラグインは `MainWindow`、`RDKit` 分子オブジェクト、`PyGraphics` アイテムに直接アクセスでき、無限のカスタマイズが可能です。
919
+ * **フルAPIアクセス:** プラグインは `MainWindow`、`RDKit` 分子オブジェクト、2D シーンの Qt グラフィックスアイテムに直接アクセスでき、無限のカスタマイズが可能です。
918
920
  * **迅速なプロトタイピング:** 新しいアルゴリズムやワークフローの自動化をその場でテストしたい研究者に最適です。
919
921
 
920
922
  ## インストールと実行
@@ -970,6 +972,7 @@ moleditpy
970
972
  * **GUIと2D描画 (PyQt6):** `QGraphicsScene`上にカスタムの`AtomItem`(原子)と`BondItem`(結合)を配置し、対話的に操作します。Undo/Redo機能は、アプリケーションの状態をシリアライズしてスタックに保存することで実現しています。
971
973
  * **化学計算 (RDKit / Open Babel):** 2DデータからRDKit分子オブジェクトを生成し、3D座標生成や分子特性計算を実行します。RDKitでの3D座標生成が失敗した際は、Open Babelにフォールバックします。重い計算処理は別スレッド (`QThread`) で実行し、GUIの応答性を維持しています。
972
974
  * **3D可視化 (PyVista / pyvistaqt):** RDKitのコンフォーマ座標からPyVistaのメッシュ(球や円柱)を生成して描画します。カスタムの`vtkInteractorStyle`を実装し、3Dビュー内での原子の直接的なドラッグ&ドロップ編集を可能にしています。
975
+ * **モジュラーアーキテクチャ:** コードベースは `core` ロジック、`ui` コンポーネント、`utils` の専用パッケージに整理されています。`MainWindow` は、状態管理・ファイル入出力・3D ビュー・ダイアログなどを専門のマネージャーオブジェクトに委譲するコンポジションハブとして機能し、長期的な保守性と検証の容易さを確保しています。
973
976
 
974
977
  ## ライセンス & 免責事項
975
978
 
@@ -67,16 +67,17 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
67
67
  * **Stereochemistry Display:** Automatically identifies and displays R/S labels for chiral centers in the 3D view after conversion.
68
68
  * **File I/O:**
69
69
  * Save and load entire sessions, including 2D/3D data and constraints, with the native `.pmeprj` project file.
70
- * Import structures from **MOL/SDF** files or **SMILES** strings.
70
+ * Import structures from **MOL/SDF/XYZ** files or **SMILES/InChI** strings.
71
71
  * Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
72
- * Export 2D and 3D views as high-resolution PNG images.
72
+ * Export 2D views as high-resolution PNG or SVG images, and 3D views as PNG images.
73
+ * Export the 3D model as **STL** or **OBJ/MTL** files for 3D printing and rendering.
73
74
  * **Security note on `.pmeraw`:** the legacy `.pmeraw` project format uses Python pickle, which can execute arbitrary code when loaded. Only open `.pmeraw` files that you created yourself; prefer the JSON-based `.pmeprj` format for sharing.
74
75
 
75
76
  ### 4. Programmable & Extensible
76
77
 
77
78
  * **Python Plugin System:** Drop your Python scripts into the plugin folder, and they instantly become part of the application menu.
78
79
  * **Downloadable Plugins:** Explore and download specialized plugins from the [Plugin Explorer](https://hiroyokoyama.github.io/moleditpy-plugins/explorer/).
79
- * **Full API Access:** Plugins have direct access to the `MainWindow`, `RDKit` molecule objects, and `PyGraphics` items, allowing for limitless customization.
80
+ * **Full API Access:** Plugins have direct access to the `MainWindow`, `RDKit` molecule objects, and the Qt graphics items of the 2D scene, allowing for limitless customization.
80
81
  * **Rapid Prototyping:** Ideal for researchers who need to test new algorithms or workflow automations on the fly.
81
82
 
82
83
  ## Installation and Execution
@@ -132,7 +133,7 @@ moleditpy
132
133
  * **GUI and 2D Drawing (PyQt6):** The editor is built on a `QGraphicsScene`, where custom `AtomItem` and `BondItem` objects are interactively manipulated. The Undo/Redo feature is implemented by serializing the application state.
133
134
  * **Chemical Calculations (RDKit / Open Babel):** RDKit is used to generate molecule objects from 2D data, perform 3D coordinate generation, and calculate properties. Open Babel serves as a fallback for 3D conversion. All heavy computations are run on a separate `QThread` to keep the GUI responsive.
134
135
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
135
- * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
136
+ * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The `MainWindow` acts as a composition hub that delegates all work to specialized manager objects (state, I/O, 3D view, dialogs), ensuring long-term maintainability and easier verification.
136
137
 
137
138
  ## License & Disclaimer
138
139
 
@@ -202,16 +203,17 @@ Additionally, please cite the plugins you used.
202
203
  * **立体化学表示:** 3D変換後、キラル中心を自動的に認識し、R/Sラベルを3Dビューに表示します。
203
204
  * **ファイル入出力:**
204
205
  * 2D/3Dデータや制約情報を含むセッション全体を、独自のプロジェクトファイル (`.pmeprj`) として保存・読み込みできます。
205
- * **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
206
+ * **MOL/SDF/XYZ**ファイルや**SMILES/InChI**文字列から構造をインポートできます。
206
207
  * 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
207
- * 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
208
+ * 2Dビューを高解像度のPNG・SVG画像として、3DビューをPNG画像としてエクスポートできます。
209
+ * 3Dモデルを**STL**または**OBJ/MTL**ファイルとしてエクスポートでき、3Dプリントやレンダリングに利用できます。
208
210
  * **`.pmeraw` に関するセキュリティ上の注意:** 旧形式の `.pmeraw` プロジェクトは Python の pickle を使用しており、読み込み時に任意のコードが実行される可能性があります。自分で作成した `.pmeraw` ファイルのみを開き、共有には JSON ベースの `.pmeprj` 形式を使用してください。
209
211
 
210
212
  ### 4. プログラマブルで拡張可能
211
213
 
212
214
  * **Pythonプラグインシステム:** Pythonスクリプトをプラグインフォルダに入れるだけで、即座にアプリケーションメニューの一部として機能します。
213
215
  * **プラグインのダウンロード:** [Plugin Explorer](https://hiroyokoyama.github.io/moleditpy-plugins/explorer/) から特化したプラグインを探索・ダウンロードできます。
214
- * **フルAPIアクセス:** プラグインは `MainWindow`、`RDKit` 分子オブジェクト、`PyGraphics` アイテムに直接アクセスでき、無限のカスタマイズが可能です。
216
+ * **フルAPIアクセス:** プラグインは `MainWindow`、`RDKit` 分子オブジェクト、2D シーンの Qt グラフィックスアイテムに直接アクセスでき、無限のカスタマイズが可能です。
215
217
  * **迅速なプロトタイピング:** 新しいアルゴリズムやワークフローの自動化をその場でテストしたい研究者に最適です。
216
218
 
217
219
  ## インストールと実行
@@ -267,6 +269,7 @@ moleditpy
267
269
  * **GUIと2D描画 (PyQt6):** `QGraphicsScene`上にカスタムの`AtomItem`(原子)と`BondItem`(結合)を配置し、対話的に操作します。Undo/Redo機能は、アプリケーションの状態をシリアライズしてスタックに保存することで実現しています。
268
270
  * **化学計算 (RDKit / Open Babel):** 2DデータからRDKit分子オブジェクトを生成し、3D座標生成や分子特性計算を実行します。RDKitでの3D座標生成が失敗した際は、Open Babelにフォールバックします。重い計算処理は別スレッド (`QThread`) で実行し、GUIの応答性を維持しています。
269
271
  * **3D可視化 (PyVista / pyvistaqt):** RDKitのコンフォーマ座標からPyVistaのメッシュ(球や円柱)を生成して描画します。カスタムの`vtkInteractorStyle`を実装し、3Dビュー内での原子の直接的なドラッグ&ドロップ編集を可能にしています。
272
+ * **モジュラーアーキテクチャ:** コードベースは `core` ロジック、`ui` コンポーネント、`utils` の専用パッケージに整理されています。`MainWindow` は、状態管理・ファイル入出力・3D ビュー・ダイアログなどを専門のマネージャーオブジェクトに委譲するコンポジションハブとして機能し、長期的な保守性と検証の容易さを確保しています。
270
273
 
271
274
  ## ライセンス & 免責事項
272
275
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "4.2.4"
8
+ version = "4.3.1"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 4.2.4
3
+ Version: 4.3.1
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
@@ -770,16 +770,17 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
770
770
  * **Stereochemistry Display:** Automatically identifies and displays R/S labels for chiral centers in the 3D view after conversion.
771
771
  * **File I/O:**
772
772
  * Save and load entire sessions, including 2D/3D data and constraints, with the native `.pmeprj` project file.
773
- * Import structures from **MOL/SDF** files or **SMILES** strings.
773
+ * Import structures from **MOL/SDF/XYZ** files or **SMILES/InChI** strings.
774
774
  * Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
775
- * Export 2D and 3D views as high-resolution PNG images.
775
+ * Export 2D views as high-resolution PNG or SVG images, and 3D views as PNG images.
776
+ * Export the 3D model as **STL** or **OBJ/MTL** files for 3D printing and rendering.
776
777
  * **Security note on `.pmeraw`:** the legacy `.pmeraw` project format uses Python pickle, which can execute arbitrary code when loaded. Only open `.pmeraw` files that you created yourself; prefer the JSON-based `.pmeprj` format for sharing.
777
778
 
778
779
  ### 4. Programmable & Extensible
779
780
 
780
781
  * **Python Plugin System:** Drop your Python scripts into the plugin folder, and they instantly become part of the application menu.
781
782
  * **Downloadable Plugins:** Explore and download specialized plugins from the [Plugin Explorer](https://hiroyokoyama.github.io/moleditpy-plugins/explorer/).
782
- * **Full API Access:** Plugins have direct access to the `MainWindow`, `RDKit` molecule objects, and `PyGraphics` items, allowing for limitless customization.
783
+ * **Full API Access:** Plugins have direct access to the `MainWindow`, `RDKit` molecule objects, and the Qt graphics items of the 2D scene, allowing for limitless customization.
783
784
  * **Rapid Prototyping:** Ideal for researchers who need to test new algorithms or workflow automations on the fly.
784
785
 
785
786
  ## Installation and Execution
@@ -835,7 +836,7 @@ moleditpy
835
836
  * **GUI and 2D Drawing (PyQt6):** The editor is built on a `QGraphicsScene`, where custom `AtomItem` and `BondItem` objects are interactively manipulated. The Undo/Redo feature is implemented by serializing the application state.
836
837
  * **Chemical Calculations (RDKit / Open Babel):** RDKit is used to generate molecule objects from 2D data, perform 3D coordinate generation, and calculate properties. Open Babel serves as a fallback for 3D conversion. All heavy computations are run on a separate `QThread` to keep the GUI responsive.
837
838
  * **3D Visualization (PyVista / pyvistaqt):** 3D rendering is achieved by generating PyVista meshes (spheres and cylinders) from RDKit conformer coordinates. A custom `vtkInteractorStyle` enables direct drag-and-drop editing of atoms in the 3D view.
838
- * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The main application logic is decomposed into reusable mixins, ensuring long-term maintainability and easier verification.
839
+ * **Modular Architecture:** The codebase is organized into dedicated packages for `core` logic, `ui` components, and `utils`. The `MainWindow` acts as a composition hub that delegates all work to specialized manager objects (state, I/O, 3D view, dialogs), ensuring long-term maintainability and easier verification.
839
840
 
840
841
  ## License & Disclaimer
841
842
 
@@ -905,16 +906,17 @@ Additionally, please cite the plugins you used.
905
906
  * **立体化学表示:** 3D変換後、キラル中心を自動的に認識し、R/Sラベルを3Dビューに表示します。
906
907
  * **ファイル入出力:**
907
908
  * 2D/3Dデータや制約情報を含むセッション全体を、独自のプロジェクトファイル (`.pmeprj`) として保存・読み込みできます。
908
- * **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
909
+ * **MOL/SDF/XYZ**ファイルや**SMILES/InChI**文字列から構造をインポートできます。
909
910
  * 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
910
- * 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
911
+ * 2Dビューを高解像度のPNG・SVG画像として、3DビューをPNG画像としてエクスポートできます。
912
+ * 3Dモデルを**STL**または**OBJ/MTL**ファイルとしてエクスポートでき、3Dプリントやレンダリングに利用できます。
911
913
  * **`.pmeraw` に関するセキュリティ上の注意:** 旧形式の `.pmeraw` プロジェクトは Python の pickle を使用しており、読み込み時に任意のコードが実行される可能性があります。自分で作成した `.pmeraw` ファイルのみを開き、共有には JSON ベースの `.pmeprj` 形式を使用してください。
912
914
 
913
915
  ### 4. プログラマブルで拡張可能
914
916
 
915
917
  * **Pythonプラグインシステム:** Pythonスクリプトをプラグインフォルダに入れるだけで、即座にアプリケーションメニューの一部として機能します。
916
918
  * **プラグインのダウンロード:** [Plugin Explorer](https://hiroyokoyama.github.io/moleditpy-plugins/explorer/) から特化したプラグインを探索・ダウンロードできます。
917
- * **フルAPIアクセス:** プラグインは `MainWindow`、`RDKit` 分子オブジェクト、`PyGraphics` アイテムに直接アクセスでき、無限のカスタマイズが可能です。
919
+ * **フルAPIアクセス:** プラグインは `MainWindow`、`RDKit` 分子オブジェクト、2D シーンの Qt グラフィックスアイテムに直接アクセスでき、無限のカスタマイズが可能です。
918
920
  * **迅速なプロトタイピング:** 新しいアルゴリズムやワークフローの自動化をその場でテストしたい研究者に最適です。
919
921
 
920
922
  ## インストールと実行
@@ -970,6 +972,7 @@ moleditpy
970
972
  * **GUIと2D描画 (PyQt6):** `QGraphicsScene`上にカスタムの`AtomItem`(原子)と`BondItem`(結合)を配置し、対話的に操作します。Undo/Redo機能は、アプリケーションの状態をシリアライズしてスタックに保存することで実現しています。
971
973
  * **化学計算 (RDKit / Open Babel):** 2DデータからRDKit分子オブジェクトを生成し、3D座標生成や分子特性計算を実行します。RDKitでの3D座標生成が失敗した際は、Open Babelにフォールバックします。重い計算処理は別スレッド (`QThread`) で実行し、GUIの応答性を維持しています。
972
974
  * **3D可視化 (PyVista / pyvistaqt):** RDKitのコンフォーマ座標からPyVistaのメッシュ(球や円柱)を生成して描画します。カスタムの`vtkInteractorStyle`を実装し、3Dビュー内での原子の直接的なドラッグ&ドロップ編集を可能にしています。
975
+ * **モジュラーアーキテクチャ:** コードベースは `core` ロジック、`ui` コンポーネント、`utils` の専用パッケージに整理されています。`MainWindow` は、状態管理・ファイル入出力・3D ビュー・ダイアログなどを専門のマネージャーオブジェクトに委譲するコンポジションハブとして機能し、長期的な保守性と検証の容易さを確保しています。
973
976
 
974
977
  ## ライセンス & 免責事項
975
978
 
@@ -11,20 +11,23 @@ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
  import ctypes
14
+ import hashlib
14
15
  import json
15
16
  import logging
16
17
  import logging.handlers
17
18
  import os
18
19
  import sys
19
20
  import argparse
20
- from typing import Any, Optional
21
+ import time
22
+ import traceback
23
+ from typing import Any, Dict, Optional
21
24
 
22
25
  from .utils.constants import VERSION
23
26
 
24
27
  # VERSION is resolved above (before Qt) so --version works without launching the app.
25
28
 
26
- from PyQt6.QtCore import QtMsgType, qInstallMessageHandler
27
- from PyQt6.QtWidgets import QApplication
29
+ from PyQt6.QtCore import QtMsgType, QThread, QTimer, qInstallMessageHandler
30
+ from PyQt6.QtWidgets import QApplication, QMessageBox
28
31
 
29
32
  from .ui.main_window import MainWindow
30
33
 
@@ -49,6 +52,109 @@ def _qt_message_handler(mode: QtMsgType, _context: Any, message: Optional[str])
49
52
  logging.log(_QT_LOG_LEVEL.get(mode, logging.WARNING), "Qt: %s", message)
50
53
 
51
54
 
55
+ class _ErrorDialogHandler(logging.Handler):
56
+ """Surface ERROR/CRITICAL log records in a dialog, once per unique message.
57
+
58
+ The uncaught-exception hook routes through here too (it logs at ERROR with
59
+ ``exc_info``), so every genuine failure — logged explicitly or bubbling out
60
+ of Qt — gets one dialog. Warnings/info never reach it (handler level is
61
+ ERROR). All state is per-instance; the one instance is created and attached
62
+ in :func:`setup_logging` (only for a real GUI run, never headless).
63
+
64
+ Guards: GUI thread + live QApplication only (a worker-thread record, where a
65
+ QMessageBox is unsafe, stays log-only); a time-windowed dedup so a fast
66
+ repeating error (e.g. from a QTimer slot) collapses to one dialog while a
67
+ genuine retry seconds later surfaces again; and ``extra={"no_dialog": True}``
68
+ to opt a record out. ``emit`` never raises.
69
+
70
+ The dialog is shown non-blocking (``show``, not ``exec``): it never spins a
71
+ nested event loop, so it cannot freeze the app — or a GUI-enabled test run —
72
+ while it is on screen.
73
+ """
74
+
75
+ # Identical errors within this many seconds share one dialog; a repeat
76
+ # after it surfaces again.
77
+ _DEDUP_WINDOW_S = 10.0
78
+
79
+ def __init__(self, log_path: Optional[str] = None) -> None:
80
+ super().__init__(level=logging.ERROR)
81
+ # signature -> monotonic timestamp of its last shown dialog.
82
+ self._last_shown: Dict[str, float] = {}
83
+ # Held so a non-blocking box is not garbage-collected before it closes.
84
+ self._open_boxes: set = set()
85
+ self._log_path = log_path
86
+
87
+ def emit(self, record: logging.LogRecord) -> None:
88
+ try:
89
+ if getattr(record, "no_dialog", False):
90
+ return
91
+ app = QApplication.instance()
92
+ if app is None or QThread.currentThread() is not app.thread():
93
+ return
94
+ if app.property("moleditpy_shutting_down"):
95
+ return
96
+
97
+ message = record.getMessage()
98
+ # Source location (as the terminal log shows), then any traceback.
99
+ detail = f"{record.pathname}:{record.lineno}"
100
+ if record.exc_info:
101
+ detail += "\n\n" + "".join(traceback.format_exception(*record.exc_info))
102
+
103
+ signature = hashlib.sha1(
104
+ (message + detail).encode("utf-8", "replace")
105
+ ).hexdigest()
106
+ now = time.monotonic()
107
+ last = self._last_shown.get(signature)
108
+ if last is not None and now - last < self._DEDUP_WINDOW_S:
109
+ return
110
+ self._last_shown[signature] = now
111
+
112
+ # Defer so a site's own QMessageBox.critical (raised right after the
113
+ # log call) is already modal when _show checks activeModalWidget.
114
+ QTimer.singleShot(0, lambda: self._show(message, detail))
115
+ except Exception: # pragma: no cover - logging must never raise
116
+ self.handleError(record)
117
+
118
+ def _details(self) -> str:
119
+ """Where to find the full error text, honest about file logging."""
120
+ if self._log_path:
121
+ return (
122
+ "Full details are in the terminal output and the log file:\n"
123
+ f"{self._log_path}"
124
+ )
125
+ return (
126
+ "Full details are in the terminal output. Enable "
127
+ "Settings ▸ Other ▸ 'Save log to file' to also keep them "
128
+ "on disk."
129
+ )
130
+
131
+ def _show(self, message: str, detail: str) -> None:
132
+ """Show one non-blocking error dialog, unless another modal is up.
133
+
134
+ Sites (and plugins) log the error and then immediately raise their own
135
+ ``QMessageBox.critical``; by the time this deferred call fires, that
136
+ modal is up, so we detect it and stay quiet — surfacing the generic
137
+ dialog only for errors nobody else reports.
138
+ """
139
+ if QApplication.activeModalWidget() is not None:
140
+ return
141
+ try:
142
+ box = QMessageBox()
143
+ box.setIcon(QMessageBox.Icon.Critical)
144
+ box.setWindowTitle("MoleditPy — Error")
145
+ box.setText(message or "An unexpected error occurred.")
146
+ box.setInformativeText(self._details())
147
+ if detail:
148
+ box.setDetailedText(detail)
149
+ box.setStandardButtons(QMessageBox.StandardButton.Ok)
150
+ box.setModal(True)
151
+ self._open_boxes.add(box)
152
+ box.finished.connect(lambda _result, b=box: self._open_boxes.discard(b))
153
+ box.show()
154
+ except Exception:
155
+ logging.debug("Failed to show error dialog", exc_info=True)
156
+
157
+
52
158
  def _read_startup_log_settings() -> tuple[bool, bool]:
53
159
  """Read log_to_file and log_level_debug from settings.json before Qt starts.
54
160
 
@@ -78,6 +184,7 @@ def setup_logging() -> None:
78
184
  force=True,
79
185
  )
80
186
 
187
+ active_log_path: Optional[str] = None
81
188
  if log_to_file:
82
189
  log_dir = os.path.join(os.path.expanduser("~"), ".moleditpy")
83
190
  try:
@@ -90,11 +197,23 @@ def setup_logging() -> None:
90
197
  fh.setFormatter(logging.Formatter(fmt))
91
198
  logging.getLogger().addHandler(fh)
92
199
  logging.info("File logging enabled: %s", log_path)
200
+ active_log_path = log_path
93
201
  except OSError as e:
94
202
  logging.warning("Could not open log file: %s", e)
95
203
 
204
+ # Surface every ERROR/CRITICAL record in a dialog (see _ErrorDialogHandler);
205
+ # uncaught exceptions flow through here too via handle_exception's log call.
206
+ # It is a GUI feature, so it is skipped in headless mode (MOLEDITPY_HEADLESS),
207
+ # where a blocking modal would hang the run.
208
+ if not os.environ.get("MOLEDITPY_HEADLESS"):
209
+ logging.getLogger().addHandler(_ErrorDialogHandler(log_path=active_log_path))
210
+
96
211
  def handle_exception(exc_type: Any, exc_value: Any, exc_traceback: Any) -> None:
97
- """Log unhandled exceptions using the configured logging system."""
212
+ """Log unhandled exceptions; the ERROR dialog handler surfaces them.
213
+
214
+ The dialog fires only for genuine failures — this log call and explicit
215
+ logging.error/critical calls — never for warnings/info.
216
+ """
98
217
  if issubclass(exc_type, KeyboardInterrupt):
99
218
  sys.__excepthook__(exc_type, exc_value, exc_traceback)
100
219
  return
@@ -113,7 +113,7 @@ class PluginManager:
113
113
  try:
114
114
  os.makedirs(self.plugin_dir)
115
115
  except OSError as e:
116
- logging.error(f"Error creating plugin directory: {e}")
116
+ logging.warning(f"Error creating plugin directory: {e}")
117
117
 
118
118
  def open_plugin_folder(self) -> None:
119
119
  """Opens the plugin directory in the OS file explorer."""
@@ -381,7 +381,9 @@ class PluginManager:
381
381
  module.initialize(context)
382
382
  except Exception as e: # plugins have full app access; catch everything to isolate faults
383
383
  status = f"Error (Init): {e}"
384
- logging.exception("Plugin %s initialize error", plugin_name)
384
+ logging.warning(
385
+ "Plugin %s initialize error", plugin_name, exc_info=True
386
+ )
385
387
  elif has_autorun:
386
388
  try:
387
389
  if self.main_window:
@@ -390,7 +392,9 @@ class PluginManager:
390
392
  status = "Skipped (No MW)"
391
393
  except Exception as e: # plugins have full app access; catch everything to isolate faults
392
394
  status = f"Error (Autorun): {e}"
393
- logging.exception("Plugin %s autorun error", plugin_name)
395
+ logging.warning(
396
+ "Plugin %s autorun error", plugin_name, exc_info=True
397
+ )
394
398
  elif not has_run:
395
399
  status = "No Entry Point"
396
400
 
@@ -409,7 +413,9 @@ class PluginManager:
409
413
  )
410
414
 
411
415
  except Exception as e: # plugins have full app access; isolate any load failure to prevent crashing discovery
412
- logging.exception("Failed to load plugin %s: %s", module_name, e)
416
+ logging.warning(
417
+ "Failed to load plugin %s: %s", module_name, e, exc_info=True
418
+ )
413
419
 
414
420
  def run_plugin(self, module: Any, main_window: Any) -> None:
415
421
  """Executes the plugin's run method (Legacy manual trigger)."""
@@ -620,7 +626,7 @@ class PluginManager:
620
626
  except (RuntimeError, ValueError, TypeError):
621
627
  continue
622
628
  except (RuntimeError, AttributeError) as e:
623
- logging.error(f"Error retrieving selected atom indices: {e}")
629
+ logging.warning(f"Error retrieving selected atom indices: {e}")
624
630
 
625
631
  return selected_indices
626
632
 
@@ -640,8 +646,11 @@ class PluginManager:
640
646
  try:
641
647
  handler["callback"]()
642
648
  except Exception as e: # plugins have full app access; catch everything to prevent data loss on document reset
643
- logging.exception(
644
- "Error in document reset handler for %s: %s", handler["plugin"], e
649
+ logging.warning(
650
+ "Error in document reset handler for %s: %s",
651
+ handler["plugin"],
652
+ e,
653
+ exc_info=True,
645
654
  )
646
655
 
647
656
  def get_plugin_info_safe(self, file_path: str) -> Dict[str, str]:
@@ -10,6 +10,7 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
+ import logging
13
14
  import os
14
15
  import shutil
15
16
  from typing import Any, Optional
@@ -213,9 +214,7 @@ class PluginManagerWindow(QDialog):
213
214
  f"Removed '{plugin.get('name', 'Unknown')}'.",
214
215
  )
215
216
  except OSError as e:
216
- QMessageBox.critical(
217
- self, "Error", f"Failed to delete plugin: {e}"
218
- )
217
+ logging.exception("Failed to delete plugin: %s", e)
219
218
  else:
220
219
  QMessageBox.warning(
221
220
  self, "Error", f"Plugin file not found:\n{filepath}"
@@ -162,7 +162,7 @@ class AlignPlaneDialog(BasePickingDialog):
162
162
  self.update_display()
163
163
 
164
164
  except (AttributeError, RuntimeError, TypeError, KeyError) as e:
165
- logging.exception("Failed to select all atoms")
165
+ logging.warning("Failed to select all atoms", exc_info=True)
166
166
  QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
167
167
 
168
168
  def update_display(self) -> None:
@@ -285,5 +285,4 @@ class AlignPlaneDialog(BasePickingDialog):
285
285
  self.show_atom_labels()
286
286
 
287
287
  except (AttributeError, RuntimeError, ValueError, TypeError) as e:
288
- logging.exception("Failed to apply align")
289
- QMessageBox.critical(self, "Error", f"Failed to apply align: {str(e)}")
288
+ logging.exception("Failed to apply align: %s", e)
@@ -276,8 +276,7 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
276
276
  )
277
277
 
278
278
  except (AttributeError, RuntimeError, ValueError, TypeError) as e:
279
- logging.exception("Failed to apply alignment")
280
- QMessageBox.critical(self, "Error", f"Failed to apply alignment: {str(e)}")
279
+ logging.exception("Failed to apply alignment: %s", e)
281
280
 
282
281
  def closeEvent(self, event: Optional[QCloseEvent]) -> None:
283
282
  """Clean up when the dialog is closed."""
@@ -223,7 +223,11 @@ class StateManager:
223
223
  self.host.view_3d_manager.draw_molecule_3d(
224
224
  self.host.view_3d_manager.current_mol
225
225
  )
226
- if self.host.view_3d_manager.plotter:
226
+ # draw_molecule_3d already preserves the camera; only refit
227
+ # on a fresh load, not on undo/redo (which restores state).
228
+ if self.host.view_3d_manager.plotter and not getattr(
229
+ self.host, "is_restoring_state", False
230
+ ):
227
231
  self.host.view_3d_manager.plotter.reset_camera()
228
232
 
229
233
  self.host.ui_manager.enable_3d_features(True)
@@ -232,7 +236,7 @@ class StateManager:
232
236
  self.host.clear_3d_view()
233
237
  self.host.ui_manager.enable_3d_features(False)
234
238
  except (RuntimeError, ValueError, TypeError) as e:
235
- logging.error(f"Could not load 3D model from state data: {e}")
239
+ logging.warning(f"Could not load 3D model from state data: {e}")
236
240
  self.host.update_status_message(f"Error loading 3D model: {e}", 5000)
237
241
  self.host.set_current_molecule(None)
238
242
  self.host.ui_manager.enable_3d_features(False)
@@ -528,7 +532,9 @@ class StateManager:
528
532
  p_state = callback()
529
533
  plugin_data[name] = p_state
530
534
  except Exception: # plugins have full app access; catch everything to prevent save corruption
531
- logging.exception("Error saving state for plugin %s", name)
535
+ logging.warning(
536
+ "Error saving state for plugin %s", name, exc_info=True
537
+ )
532
538
 
533
539
  if plugin_data:
534
540
  json_data["plugins"] = plugin_data
@@ -564,7 +570,9 @@ class StateManager:
564
570
  try:
565
571
  load_hand(p_state)
566
572
  except Exception: # plugins have full app access; catch everything to prevent load corruption
567
- logging.exception("Error loading state for plugin %s", name)
573
+ logging.warning(
574
+ "Error loading state for plugin %s", name, exc_info=True
575
+ )
568
576
  else:
569
577
  # No handler found (plugin disabled or missing)
570
578
  # Preserve data so it's not lost on next save
@@ -671,5 +679,5 @@ class StateManager:
671
679
  "Suppressed non-critical error", exc_info=True
672
680
  )
673
681
  except (RuntimeError, ValueError, TypeError, binascii.Error) as e:
674
- logging.error(f"Could not restore 3D molecular data: {e}")
682
+ logging.warning(f"Could not restore 3D molecular data: {e}")
675
683
  self.host.set_current_molecule(None)
@@ -96,7 +96,7 @@ class BondItem(QGraphicsItem):
96
96
  view.viewport().update() # type: ignore[union-attr]
97
97
 
98
98
  except (AttributeError, RuntimeError, TypeError):
99
- logging.exception("Error in BondItem.set_stereo")
99
+ logging.warning("Error in BondItem.set_stereo", exc_info=True)
100
100
  # Continue without crashing
101
101
  self.stereo = new_stereo
102
102
 
@@ -604,7 +604,7 @@ class BondItem(QGraphicsItem):
604
604
  self.setPos(self.atom1.pos())
605
605
  self.update()
606
606
  except (AttributeError, RuntimeError):
607
- logging.exception("Error updating bond position")
607
+ logging.warning("Error updating bond position", exc_info=True)
608
608
  # Continue without crashing
609
609
 
610
610
  def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent) -> None: # type: ignore[override]
@@ -390,7 +390,9 @@ class ComputeManager:
390
390
  try:
391
391
  success = bool(entry["callback"](mol))
392
392
  except Exception: # plugins have full app access; isolate failures
393
- logging.exception("Plugin optimization method '%s' failed", label)
393
+ logging.warning(
394
+ "Plugin optimization method '%s' failed", label, exc_info=True
395
+ )
394
396
  self._refresh_ui_state()
395
397
  self.host.update_status_message(
396
398
  f"Plugin optimization '{label}' failed (see log)."
@@ -431,7 +433,7 @@ class ComputeManager:
431
433
  try:
432
434
  Chem.SanitizeMol(mol)
433
435
  except (ValueError, RuntimeError) as e:
434
- logging.error(f"Sanitization failed: {e}")
436
+ logging.warning(f"Sanitization failed: {e}")
435
437
  self.host.statusBar().showMessage("Error: Invalid chemical structure.") # type: ignore[union-attr]
436
438
  return None
437
439
  return mol
@@ -587,7 +589,8 @@ class ComputeManager:
587
589
  msg = str(message)
588
590
 
589
591
  self._remove_calculating_text()
590
- self._restore_button_ui()
592
+ # Full restore like the success path, else Export stays disabled on error.
593
+ self._refresh_ui_state()
591
594
 
592
595
  if msg == "Halt" or msg == "Halted":
593
596
  self.host.statusBar().showMessage("Halted") # type: ignore[union-attr]