moleditpy 2.8.0__tar.gz → 2.8.0a1__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 (66) hide show
  1. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/PKG-INFO +3 -4
  2. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/README.md +1 -1
  3. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/pyproject.toml +2 -3
  4. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/MoleditPy.egg-info/PKG-INFO +3 -4
  5. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/MoleditPy.egg-info/requires.txt +1 -6
  6. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/calculation_worker.py +14 -41
  7. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/constants.py +1 -1
  8. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_compute.py +53 -79
  9. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_main_init.py +0 -3
  10. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/LICENSE +0 -0
  11. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/setup.cfg +0 -0
  12. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/MoleditPy.egg-info/SOURCES.txt +0 -0
  13. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  14. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  15. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/MoleditPy.egg-info/top_level.txt +0 -0
  16. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/__init__.py +0 -0
  17. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/__main__.py +0 -0
  18. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/assets/file_icon.ico +0 -0
  19. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/assets/icon.icns +0 -0
  20. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/assets/icon.ico +0 -0
  21. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/assets/icon.png +0 -0
  22. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/main.py +0 -0
  23. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/__init__.py +0 -0
  24. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/about_dialog.py +0 -0
  25. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/align_plane_dialog.py +0 -0
  26. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/alignment_dialog.py +0 -0
  27. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/analysis_window.py +0 -0
  28. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/angle_dialog.py +0 -0
  29. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/atom_item.py +0 -0
  30. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/bond_item.py +0 -0
  31. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/bond_length_dialog.py +0 -0
  32. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/color_settings_dialog.py +0 -0
  33. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
  34. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/custom_interactor_style.py +0 -0
  35. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
  36. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
  37. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/dihedral_dialog.py +0 -0
  38. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window.py +0 -0
  39. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_app_state.py +0 -0
  40. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
  41. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
  42. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
  43. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_export.py +0 -0
  44. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
  45. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_project_io.py +0 -0
  46. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_string_importers.py +0 -0
  47. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_ui_manager.py +0 -0
  48. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_view_3d.py +0 -0
  49. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
  50. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/mirror_dialog.py +0 -0
  51. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/mol_geometry.py +0 -0
  52. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/molecular_data.py +0 -0
  53. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/molecule_scene.py +0 -0
  54. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/move_group_dialog.py +0 -0
  55. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
  56. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/planarize_dialog.py +0 -0
  57. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/plugin_interface.py +0 -0
  58. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/plugin_manager.py +0 -0
  59. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/plugin_manager_window.py +0 -0
  60. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/settings_dialog.py +0 -0
  61. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/sip_isdeleted_safe.py +0 -0
  62. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/template_preview_item.py +0 -0
  63. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/template_preview_view.py +0 -0
  64. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/translation_dialog.py +0 -0
  65. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/user_template_dialog.py +0 -0
  66. {moleditpy-2.8.0 → moleditpy-2.8.0a1}/src/moleditpy/modules/zoomable_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.8.0
3
+ Version: 2.8.0a1
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
@@ -694,8 +694,7 @@ Requires-Python: <3.15,>=3.9
694
694
  Description-Content-Type: text/markdown
695
695
  License-File: LICENSE
696
696
  Requires-Dist: numpy
697
- Requires-Dist: pyqt6<6.10; sys_platform == "darwin"
698
- Requires-Dist: pyqt6<6.11; sys_platform != "darwin"
697
+ Requires-Dist: pyqt6<6.11
699
698
  Requires-Dist: pyvista<0.48
700
699
  Requires-Dist: pyvistaqt<0.12
701
700
  Requires-Dist: rdkit<2025.10
@@ -713,7 +712,7 @@ Dynamic: license-file
713
712
  ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-73%25-green)
714
713
  ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
715
714
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
- ![Pylint Score](https://img.shields.io/badge/pylint-8.63%2F10-brightgreen)
715
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.67%2F10-brightgreen)
717
716
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
718
717
 
719
718
  [🇯🇵 日本語 (Japanese)](#japanese)
@@ -9,7 +9,7 @@
9
9
  ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-73%25-green)
10
10
  ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
11
11
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
12
- ![Pylint Score](https://img.shields.io/badge/pylint-8.63%2F10-brightgreen)
12
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.67%2F10-brightgreen)
13
13
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
14
14
 
15
15
  [🇯🇵 日本語 (Japanese)](#japanese)
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "2.8.0"
8
+ version = "2.8.0a1"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -34,8 +34,7 @@ classifiers = [
34
34
 
35
35
  dependencies = [
36
36
  "numpy",
37
- "pyqt6 < 6.10; sys_platform == 'darwin'",
38
- "pyqt6 < 6.11; sys_platform != 'darwin'",
37
+ "pyqt6 < 6.11",
39
38
  "pyvista < 0.48",
40
39
  "pyvistaqt < 0.12",
41
40
  "rdkit < 2025.10",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.8.0
3
+ Version: 2.8.0a1
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
@@ -694,8 +694,7 @@ Requires-Python: <3.15,>=3.9
694
694
  Description-Content-Type: text/markdown
695
695
  License-File: LICENSE
696
696
  Requires-Dist: numpy
697
- Requires-Dist: pyqt6<6.10; sys_platform == "darwin"
698
- Requires-Dist: pyqt6<6.11; sys_platform != "darwin"
697
+ Requires-Dist: pyqt6<6.11
699
698
  Requires-Dist: pyvista<0.48
700
699
  Requires-Dist: pyvistaqt<0.12
701
700
  Requires-Dist: rdkit<2025.10
@@ -713,7 +712,7 @@ Dynamic: license-file
713
712
  ![Core Logic Coverage](https://img.shields.io/badge/core_logic_coverage-73%25-green)
714
713
  ![Overall Coverage](https://img.shields.io/badge/coverage-50%25-orange)
715
714
  ![GUI Status](https://img.shields.io/badge/GUI-Manually_Verified-blue)
716
- ![Pylint Score](https://img.shields.io/badge/pylint-8.63%2F10-brightgreen)
715
+ ![Pylint Score](https://img.shields.io/badge/pylint-8.67%2F10-brightgreen)
717
716
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/moleditpy?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/moleditpy)
718
717
 
719
718
  [🇯🇵 日本語 (Japanese)](#japanese)
@@ -1,11 +1,6 @@
1
1
  numpy
2
+ pyqt6<6.11
2
3
  pyvista<0.48
3
4
  pyvistaqt<0.12
4
5
  rdkit<2025.10
5
6
  openbabel-wheel<3.2
6
-
7
- [:sys_platform != "darwin"]
8
- pyqt6<6.11
9
-
10
- [:sys_platform == "darwin"]
11
- pyqt6<6.10
@@ -21,40 +21,21 @@ from rdkit import Chem
21
21
  from rdkit.Chem import AllChem, rdGeometry
22
22
  from rdkit.DistanceGeometry import DoTriangleSmoothing
23
23
 
24
- try: # pragma: no cover
24
+ try:
25
25
  from . import OBABEL_AVAILABLE
26
- except Exception: # pragma: no cover
26
+ except Exception:
27
27
  from modules import OBABEL_AVAILABLE
28
28
  # Only import pybel on demand — `moleditpy` itself doesn't expose `pybel`.
29
- if OBABEL_AVAILABLE: # pragma: no cover
29
+ if OBABEL_AVAILABLE:
30
30
  try:
31
31
  import os
32
- import glob
33
32
  import openbabel
34
33
  from openbabel import pybel
35
34
 
36
35
  # The python wheel often misses setting up the data directory.
37
- # Check multiple potential locations for BABEL_DATADIR.
38
- ob_base = os.path.dirname(openbabel.__file__)
39
- data_candidates = [
40
- os.path.join(ob_base, "bin", "data"), # Typical for Windows wheel
41
- os.path.join(ob_base, "share", "openbabel", "*") # Typical for macOS/Linux
42
- ]
43
-
44
- found_datadir = None
45
- for pattern in data_candidates:
46
- matches = glob.glob(pattern)
47
- for m in matches:
48
- if os.path.isdir(m):
49
- found_datadir = m
50
- break
51
- if found_datadir:
52
- break
53
-
54
- if found_datadir:
55
- os.environ["BABEL_DATADIR"] = found_datadir
56
-
57
- os.environ["BABEL_LIBDIR"] = ob_base
36
+ # It typically places parameter files in openbabel/bin/data.
37
+ os.environ["BABEL_DATADIR"] = os.path.join(os.path.dirname(openbabel.__file__), "bin", "data")
38
+ os.environ["BABEL_LIBDIR"] = os.path.dirname(openbabel.__file__)
58
39
  except Exception:
59
40
  # If import fails here, disable OBABEL locally; avoid raising
60
41
  pybel = None
@@ -62,7 +43,7 @@ if OBABEL_AVAILABLE: # pragma: no cover
62
43
  print(
63
44
  "Warning: openbabel.pybel not available. Open Babel fallback and OBabel-based options will be disabled."
64
45
  )
65
- else: # pragma: no cover
46
+ else:
66
47
  pybel = None
67
48
 
68
49
 
@@ -191,7 +172,7 @@ def _adjust_collision_avoidance(rd_mol, check_halted_cb, safe_status_cb):
191
172
  safe_status_cb("Collision avoidance completed.")
192
173
  except WorkerHaltError:
193
174
  raise
194
- except Exception as e: # pragma: no cover
175
+ except Exception as e:
195
176
  import traceback
196
177
  traceback.print_exc()
197
178
  safe_status_cb(f"Collision avoidance warning: {e}")
@@ -295,11 +276,11 @@ class CalculationWorker(QObject):
295
276
  error = pyqtSignal(object)
296
277
  start_work = pyqtSignal(str, object)
297
278
 
298
- def __init__(self, parent=None): # pragma: no cover
279
+ def __init__(self, parent=None):
299
280
  super().__init__(parent)
300
281
  try:
301
282
  self.start_work.connect(self.run_calculation)
302
- except Exception:
283
+ except Exception: # pragma: no cover
303
284
  import traceback
304
285
  traceback.print_exc()
305
286
 
@@ -327,7 +308,7 @@ class CalculationWorker(QObject):
327
308
  return False
328
309
 
329
310
  # Safe-emission helpers: do nothing if this worker has been halted.
330
- def _safe_status(msg): # pragma: no cover
311
+ def _safe_status(msg):
331
312
  try:
332
313
  if _check_halted():
333
314
  raise WorkerHaltError("Halted")
@@ -393,13 +374,13 @@ class CalculationWorker(QObject):
393
374
  worker_id = None
394
375
 
395
376
  _warned_no_worker_id = False
396
- if worker_id is None: # pragma: no cover
377
+ if worker_id is None:
397
378
  try:
398
379
  # best-effort, swallow any errors (signals may not be connected)
399
380
  self.status_update.emit(
400
381
  "Warning: worker started without 'worker_id'; will listen for global halt signals."
401
382
  )
402
- except Exception:
383
+ except Exception: # pragma: no cover
403
384
  import traceback
404
385
  traceback.print_exc()
405
386
  _warned_no_worker_id = True
@@ -505,16 +486,8 @@ class CalculationWorker(QObject):
505
486
  method_key = "GHEMICAL"
506
487
 
507
488
  if backend == "OBABEL":
508
- try:
509
- mol.SetProp("_pme_optimization_method", opt_method)
510
- except Exception:
511
- pass
512
489
  opt_success = _iterative_optimize_obabel(mol, method_key, _check_halted, _safe_status)
513
490
  else:
514
- try:
515
- mol.SetProp("_pme_optimization_method", opt_method)
516
- except Exception:
517
- pass
518
491
  opt_success = _iterative_optimize(mol, method_key, _check_halted, _safe_status)
519
492
 
520
493
  if not opt_success:
@@ -1108,7 +1081,7 @@ class CalculationWorker(QObject):
1108
1081
  conf_id = AllChem.EmbedMolecule(mol, basic_params)
1109
1082
  else:
1110
1083
  conf_id = -1
1111
- except Exception: # pragma: no cover
1084
+ except Exception: # pragma: no cover
1112
1085
  import traceback
1113
1086
  traceback.print_exc()
1114
1087
  """
@@ -16,7 +16,7 @@ from PyQt6.QtGui import QColor, QFont
16
16
  from rdkit import Chem
17
17
 
18
18
  # Version
19
- VERSION = "2.8.0"
19
+ VERSION = "2.8.0a1"
20
20
 
21
21
  ATOM_RADIUS = 18
22
22
  BOND_OFFSET = 3.5
@@ -1024,14 +1024,17 @@ class MainWindowCompute(object):
1024
1024
  traceback.print_exc()
1025
1025
  if not opt_method:
1026
1026
  opt_method = getattr(self, "optimization_method", None)
1027
- # Store the optimization method using its user-friendly UI label if available.
1027
+ # normalize common forms
1028
1028
  if opt_method:
1029
- try:
1030
- # Look up the UI label (e.g. "MMFF94s (RDKit)") from the internal ID (e.g. "MMFF_RDKIT")
1031
- method_key = str(opt_method).upper()
1032
- label = self.opt3d_method_labels.get(method_key, opt_method)
1033
- self.last_successful_optimization_method = label
1034
- except Exception:
1029
+ om = str(opt_method).upper()
1030
+ if "MMFF94S" in om or "MMFF_RDKIT" in om:
1031
+ self.last_successful_optimization_method = "MMFF94s"
1032
+ elif "MMFF94" in om:
1033
+ self.last_successful_optimization_method = "MMFF94"
1034
+ elif "UFF" in om:
1035
+ self.last_successful_optimization_method = "UFF"
1036
+ else:
1037
+ # store raw value otherwise
1035
1038
  self.last_successful_optimization_method = opt_method
1036
1039
  except Exception:
1037
1040
  # non-fatal
@@ -1119,10 +1122,7 @@ class MainWindowCompute(object):
1119
1122
  import traceback
1120
1123
  traceback.print_exc()
1121
1124
 
1122
- if self.last_successful_optimization_method:
1123
- self.statusBar().showMessage(f"3D calculation ({self.last_successful_optimization_method}) successful.")
1124
- else:
1125
- self.statusBar().showMessage("3D calculation successful.")
1125
+ # self.statusBar().showMessage("3D conversion successful.")
1126
1126
  self.convert_button.setEnabled(True)
1127
1127
  # Restore button text/handlers in case they were changed to Halt
1128
1128
  try:
@@ -1203,18 +1203,11 @@ class MainWindowCompute(object):
1203
1203
  return
1204
1204
 
1205
1205
  # Clear temporary plotter content and remove calculating text if present
1206
- # Only clear the entire plotter if we don't have an existing molecule to fall back to.
1207
1206
  try:
1208
- if self.current_mol is None:
1209
- self.plotter.clear()
1210
- else:
1211
- # If we have a molecule, just re-render to make sure it's drawn correctly
1212
- # (in case partial actors were added during 'calculating' phase)
1213
- self.plotter.render()
1207
+ self.plotter.clear()
1214
1208
  except Exception: # pragma: no cover
1215
1209
  import traceback
1216
1210
  traceback.print_exc()
1217
- traceback.print_exc()
1218
1211
 
1219
1212
  # Also attempt to explicitly remove the calculating text actor if it was stored
1220
1213
  try:
@@ -1289,7 +1282,7 @@ class MainWindowCompute(object):
1289
1282
  QMessageBox.StandardButton.Yes,
1290
1283
  )
1291
1284
  if reply == QMessageBox.StandardButton.Yes:
1292
- self._temp_optimization_method = "UFF_RDKIT"
1285
+ self.set_optimization_method("UFF_RDKIT")
1293
1286
  self.optimize_3d_structure()
1294
1287
  # Return immediately so the rest of the error cleanup doesn't disable buttons permanently
1295
1288
  return
@@ -1332,70 +1325,51 @@ class MainWindowCompute(object):
1332
1325
  import traceback
1333
1326
  traceback.print_exc()
1334
1327
 
1335
- # On calculation error we should NOT enable 3D-only features IF we have no molecule.
1336
- # However, if we ALREADY had a molecule (e.g. optimization failed but previous 3D was valid),
1337
- # we should keep them enabled.
1338
- if self.current_mol is None:
1339
- try:
1340
- if hasattr(self, "optimize_3d_button"):
1341
- self.optimize_3d_button.setEnabled(False)
1342
- except Exception: # pragma: no cover
1343
- import traceback
1344
- traceback.print_exc()
1328
+ # On calculation error we should NOT enable 3D-only features.
1329
+ # Explicitly disable Optimize and Export so the user can't try to operate
1330
+ # on an invalid or missing 3D molecule.
1331
+ try:
1332
+ if hasattr(self, "optimize_3d_button"):
1333
+ self.optimize_3d_button.setEnabled(False)
1334
+ except Exception: # pragma: no cover
1335
+ import traceback
1336
+ traceback.print_exc()
1345
1337
 
1346
- try:
1347
- if hasattr(self, "export_button"):
1348
- self.export_button.setEnabled(False)
1349
- except Exception: # pragma: no cover
1350
- import traceback
1351
- traceback.print_exc()
1338
+ try:
1339
+ if hasattr(self, "export_button"):
1340
+ self.export_button.setEnabled(False)
1341
+ except Exception: # pragma: no cover
1342
+ import traceback
1343
+ traceback.print_exc()
1352
1344
 
1353
- # Keep 3D feature buttons disabled to avoid inconsistent UI state
1354
- try:
1355
- self._enable_3d_features(False)
1356
- except Exception: # pragma: no cover
1357
- import traceback
1358
- traceback.print_exc()
1345
+ # Keep 3D feature buttons disabled to avoid inconsistent UI state
1346
+ try:
1347
+ self._enable_3d_features(False)
1348
+ except Exception: # pragma: no cover
1349
+ import traceback
1350
+ traceback.print_exc()
1359
1351
 
1360
- # Keep 3D edit actions disabled (no molecule to edit)
1361
- try:
1362
- self._enable_3d_edit_actions(False)
1363
- except Exception: # pragma: no cover
1364
- import traceback
1365
- traceback.print_exc()
1366
- else:
1367
- # We HAVE a molecule, ensure features are enabled
1368
- try:
1369
- self._enable_3d_features(True)
1370
- except Exception: # pragma: no cover
1371
- import traceback
1372
- traceback.print_exc()
1352
+ # Keep 3D edit actions disabled (no molecule to edit)
1353
+ try:
1354
+ self._enable_3d_edit_actions(False)
1355
+ except Exception: # pragma: no cover
1356
+ import traceback
1357
+ traceback.print_exc()
1373
1358
 
1374
- # Some menu items are explicitly disabled on error IF no molecule exists
1375
- if self.current_mol is None:
1376
- try:
1377
- if hasattr(self, "analysis_action"):
1378
- self.analysis_action.setEnabled(False)
1379
- except Exception: # pragma: no cover
1380
- import traceback
1381
- traceback.print_exc()
1359
+ # Some menu items are explicitly disabled on error
1360
+ try:
1361
+ if hasattr(self, "analysis_action"):
1362
+ self.analysis_action.setEnabled(False)
1363
+ except Exception: # pragma: no cover
1364
+ import traceback
1365
+ traceback.print_exc()
1382
1366
 
1383
- try:
1384
- if hasattr(self, "edit_3d_action"):
1385
- self.edit_3d_action.setEnabled(False)
1386
- except Exception: # pragma: no cover
1387
- import traceback
1388
- traceback.print_exc()
1389
- else:
1390
- # Ensure they are enabled if we have a molecule
1391
- try:
1392
- if hasattr(self, "analysis_action"):
1393
- self.analysis_action.setEnabled(True)
1394
- if hasattr(self, "edit_3d_action"):
1395
- self.edit_3d_action.setEnabled(True)
1396
- except Exception: # pragma: no cover
1397
- import traceback
1398
- traceback.print_exc()
1367
+ try:
1368
+ if hasattr(self, "edit_3d_action"):
1369
+ self.edit_3d_action.setEnabled(False)
1370
+ except Exception: # pragma: no cover
1371
+ import traceback
1372
+ traceback.print_exc()
1399
1373
 
1400
1374
  # Force a UI refresh
1401
1375
  try:
@@ -1484,9 +1484,6 @@ class MainWindowMainInit(object):
1484
1484
  import traceback
1485
1485
  traceback.print_exc()
1486
1486
 
1487
- # If Open Babel is not available, disable Open Babel-based optimization methods
1488
- if key.endswith("_OBABEL") and not OBABEL_AVAILABLE:
1489
- action.setEnabled(False)
1490
1487
  action.triggered.connect(
1491
1488
  lambda checked, m=key: self.set_optimization_method(m)
1492
1489
  )
File without changes
File without changes