MoleditPy-linux 4.2.3__py3-none-any.whl → 4.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moleditpy_linux/main.py +123 -4
- moleditpy_linux/plugins/plugin_manager.py +16 -7
- moleditpy_linux/plugins/plugin_manager_window.py +2 -3
- moleditpy_linux/ui/align_plane_dialog.py +2 -3
- moleditpy_linux/ui/alignment_dialog.py +1 -2
- moleditpy_linux/ui/app_state.py +13 -5
- moleditpy_linux/ui/bond_item.py +2 -2
- moleditpy_linux/ui/compute_logic.py +6 -3
- moleditpy_linux/ui/constrained_optimization_dialog.py +5 -8
- moleditpy_linux/ui/custom_interactor_style.py +15 -13
- moleditpy_linux/ui/dialog_3d_picking_mixin.py +1 -1
- moleditpy_linux/ui/dialog_logic.py +2 -3
- moleditpy_linux/ui/edit_actions_logic.py +1 -1
- moleditpy_linux/ui/main_window.py +1 -1
- moleditpy_linux/ui/main_window_init.py +6 -5
- moleditpy_linux/ui/mirror_dialog.py +2 -4
- moleditpy_linux/ui/molecular_scene_handler.py +7 -7
- moleditpy_linux/ui/molecule_scene.py +3 -3
- moleditpy_linux/ui/move_group_dialog.py +1 -1
- moleditpy_linux/ui/move_selected_atoms_dialog.py +1 -1
- moleditpy_linux/ui/planarize_dialog.py +2 -2
- moleditpy_linux/ui/plugin_menu_manager.py +70 -52
- moleditpy_linux/ui/template_preview_view.py +2 -2
- moleditpy_linux/ui/translation_dialog.py +1 -1
- moleditpy_linux/ui/ui_manager.py +6 -1
- moleditpy_linux/ui/user_template_dialog.py +8 -10
- moleditpy_linux/ui/view_3d_logic.py +17 -13
- {moleditpy_linux-4.2.3.dist-info → moleditpy_linux-4.3.0.dist-info}/METADATA +1 -1
- {moleditpy_linux-4.2.3.dist-info → moleditpy_linux-4.3.0.dist-info}/RECORD +33 -33
- {moleditpy_linux-4.2.3.dist-info → moleditpy_linux-4.3.0.dist-info}/WHEEL +1 -1
- {moleditpy_linux-4.2.3.dist-info → moleditpy_linux-4.3.0.dist-info}/entry_points.txt +0 -0
- {moleditpy_linux-4.2.3.dist-info → moleditpy_linux-4.3.0.dist-info}/licenses/LICENSE +0 -0
- {moleditpy_linux-4.2.3.dist-info → moleditpy_linux-4.3.0.dist-info}/top_level.txt +0 -0
moleditpy_linux/main.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
644
|
-
"Error in document reset handler for %s: %s",
|
|
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
|
-
|
|
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.
|
|
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."""
|
moleditpy_linux/ui/app_state.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
682
|
+
logging.warning(f"Could not restore 3D molecular data: {e}")
|
|
675
683
|
self.host.set_current_molecule(None)
|
moleditpy_linux/ui/bond_item.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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]
|
|
@@ -156,7 +156,7 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
156
156
|
self.ff_combo.setCurrentText("MMFF94s")
|
|
157
157
|
|
|
158
158
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
159
|
-
logging.
|
|
159
|
+
logging.warning("Could not set default force field", exc_info=True)
|
|
160
160
|
|
|
161
161
|
def init_ui(self) -> None:
|
|
162
162
|
"""Build the constrained optimization dialog with constraint table and controls."""
|
|
@@ -528,9 +528,7 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
528
528
|
add_torsion_constraint = ff.UFFAddTorsionConstraint
|
|
529
529
|
|
|
530
530
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
531
|
-
|
|
532
|
-
self, "Error", f"Failed to initialize force field {ff_name}: {e}"
|
|
533
|
-
)
|
|
531
|
+
logging.exception("Failed to initialize force field %s: %s", ff_name, e)
|
|
534
532
|
return
|
|
535
533
|
|
|
536
534
|
# Add constraints
|
|
@@ -578,8 +576,7 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
578
576
|
)
|
|
579
577
|
|
|
580
578
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
581
|
-
|
|
582
|
-
logging.error("Failed to add constraints", exc_info=True)
|
|
579
|
+
logging.exception("Failed to add constraints: %s", e)
|
|
583
580
|
return
|
|
584
581
|
|
|
585
582
|
# Execute optimization
|
|
@@ -598,7 +595,7 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
598
595
|
self._opt_thread.start()
|
|
599
596
|
|
|
600
597
|
except Exception as e:
|
|
601
|
-
|
|
598
|
+
logging.exception("Failed to start optimization: %s", e)
|
|
602
599
|
self.optimize_button.setEnabled(True)
|
|
603
600
|
|
|
604
601
|
def _on_optimization_error(self, err_msg: str) -> None:
|
|
@@ -673,7 +670,7 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
|
|
|
673
670
|
logging.warning("Failed to save constraints post-optimization: %s", e)
|
|
674
671
|
|
|
675
672
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
676
|
-
|
|
673
|
+
logging.exception("Optimization failed: %s", e)
|
|
677
674
|
|
|
678
675
|
def closeEvent(self, event: Any) -> None:
|
|
679
676
|
"""Delegate window close to reject."""
|
|
@@ -120,7 +120,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
120
120
|
move_group_dialog = widget
|
|
121
121
|
break
|
|
122
122
|
except (AttributeError, RuntimeError, TypeError):
|
|
123
|
-
logging.
|
|
123
|
+
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
124
124
|
|
|
125
125
|
if move_group_dialog and move_group_dialog.group_atoms:
|
|
126
126
|
# Group drag if selected
|
|
@@ -401,7 +401,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
401
401
|
move_group_dialog = widget
|
|
402
402
|
break
|
|
403
403
|
except (AttributeError, RuntimeError, TypeError):
|
|
404
|
-
logging.
|
|
404
|
+
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
405
405
|
if move_group_dialog and getattr(
|
|
406
406
|
move_group_dialog, "is_dragging_group_vtk", False
|
|
407
407
|
):
|
|
@@ -497,7 +497,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
497
497
|
move_group_dialog = widget
|
|
498
498
|
break
|
|
499
499
|
except (AttributeError, RuntimeError, TypeError):
|
|
500
|
-
logging.
|
|
500
|
+
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
501
501
|
# Prevent multi-click issues
|
|
502
502
|
if move_group_dialog:
|
|
503
503
|
if getattr(
|
|
@@ -509,7 +509,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
509
509
|
try:
|
|
510
510
|
move_group_dialog.on_atom_picked(clicked_atom)
|
|
511
511
|
except (AttributeError, RuntimeError, TypeError, ValueError) as e:
|
|
512
|
-
logging.
|
|
512
|
+
logging.warning(f"Error in toggle: {e}")
|
|
513
513
|
# Reset if multi-clicked without drag
|
|
514
514
|
move_group_dialog.is_dragging_group_vtk = False
|
|
515
515
|
move_group_dialog.drag_start_pos_vtk = None
|
|
@@ -589,7 +589,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
589
589
|
|
|
590
590
|
QTimer.singleShot(0, _deferred_group_redraw)
|
|
591
591
|
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
592
|
-
logging.
|
|
592
|
+
logging.warning("Error finalizing group drag", exc_info=True)
|
|
593
593
|
else:
|
|
594
594
|
# No drag = click only -> toggle
|
|
595
595
|
clicked_atom = getattr(move_group_dialog, "drag_atom_idx_vtk", None)
|
|
@@ -597,7 +597,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
597
597
|
try:
|
|
598
598
|
move_group_dialog.on_atom_picked(clicked_atom)
|
|
599
599
|
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
600
|
-
logging.
|
|
600
|
+
logging.warning("Error in toggle", exc_info=True)
|
|
601
601
|
|
|
602
602
|
# Background click: deselect Move Group
|
|
603
603
|
if move_group_dialog and not getattr(
|
|
@@ -725,7 +725,9 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
725
725
|
),
|
|
726
726
|
)
|
|
727
727
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
728
|
-
logging.
|
|
728
|
+
logging.warning(
|
|
729
|
+
"Caught exception in " + __file__, exc_info=True
|
|
730
|
+
)
|
|
729
731
|
|
|
730
732
|
# Defer the redraw + undo push out of the VTK observer
|
|
731
733
|
# callback to prevent re-entrant plotter.render() deadlock.
|
|
@@ -735,7 +737,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
735
737
|
try:
|
|
736
738
|
mw.view_3d_manager.draw_molecule_3d(_atom_mol)
|
|
737
739
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
738
|
-
logging.
|
|
740
|
+
logging.warning(
|
|
739
741
|
"Caught exception in " + __file__, exc_info=True
|
|
740
742
|
)
|
|
741
743
|
mw.edit_actions_manager.push_undo_state()
|
|
@@ -760,7 +762,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
760
762
|
try:
|
|
761
763
|
fn()
|
|
762
764
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
763
|
-
logging.
|
|
765
|
+
logging.warning(
|
|
764
766
|
"Caught exception in " + __file__, exc_info=True
|
|
765
767
|
)
|
|
766
768
|
|
|
@@ -790,13 +792,13 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
790
792
|
if hasattr(move_group_dialog, "drag_atom_idx_vtk"): # Safe
|
|
791
793
|
delattr(move_group_dialog, "drag_atom_idx_vtk")
|
|
792
794
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
793
|
-
logging.
|
|
795
|
+
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
794
796
|
|
|
795
797
|
# Update cursor after release
|
|
796
798
|
try:
|
|
797
799
|
mw.view_3d_manager.plotter.setCursor(Qt.CursorShape.ArrowCursor)
|
|
798
800
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
799
|
-
logging.
|
|
801
|
+
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
800
802
|
|
|
801
803
|
# Restore focus to 2D view
|
|
802
804
|
if mw and mw.init_manager.view_2d:
|
|
@@ -820,7 +822,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
820
822
|
move_group_dialog = widget
|
|
821
823
|
break
|
|
822
824
|
except (AttributeError, RuntimeError, TypeError):
|
|
823
|
-
logging.
|
|
825
|
+
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
824
826
|
if move_group_dialog and getattr(
|
|
825
827
|
move_group_dialog, "is_rotating_group_vtk", False
|
|
826
828
|
):
|
|
@@ -935,7 +937,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
935
937
|
move_group_dialog.show_atom_labels()
|
|
936
938
|
mw.edit_actions_manager.push_undo_state()
|
|
937
939
|
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
938
|
-
logging.
|
|
940
|
+
logging.warning("Error finalizing group rotation", exc_info=True)
|
|
939
941
|
|
|
940
942
|
# Reset state
|
|
941
943
|
move_group_dialog.is_rotating_group_vtk = False
|
|
@@ -100,7 +100,7 @@ class Dialog3DPickingMixin:
|
|
|
100
100
|
return False
|
|
101
101
|
|
|
102
102
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
103
|
-
logging.
|
|
103
|
+
logging.warning("Error in eventFilter", exc_info=True)
|
|
104
104
|
return False
|
|
105
105
|
|
|
106
106
|
# Add movement tracking for smart selection
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
|
+
import logging
|
|
17
18
|
import os
|
|
18
19
|
import sys
|
|
19
20
|
from typing import Any, List, Literal, Optional, cast
|
|
@@ -187,9 +188,7 @@ class DialogManager:
|
|
|
187
188
|
)
|
|
188
189
|
|
|
189
190
|
except (AttributeError, RuntimeError, ValueError) as e:
|
|
190
|
-
|
|
191
|
-
self.host, "Error", f"Failed to save template: {str(e)}"
|
|
192
|
-
)
|
|
191
|
+
logging.exception("Failed to save template: %s", e)
|
|
193
192
|
|
|
194
193
|
def _show_modeless_dialog(self, dialog: QDialog) -> None:
|
|
195
194
|
"""Show a modeless dialog on top, especially important for macOS."""
|
|
@@ -986,7 +986,7 @@ class EditActionsManager:
|
|
|
986
986
|
):
|
|
987
987
|
problem_map[atom_id] = True
|
|
988
988
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
989
|
-
logging.
|
|
989
|
+
logging.warning(f"Error during chemistry problem detection: {e}")
|
|
990
990
|
|
|
991
991
|
return problem_map
|
|
992
992
|
|
|
@@ -260,7 +260,7 @@ class MainWindow(QMainWindow):
|
|
|
260
260
|
self.state_manager.get_current_state()
|
|
261
261
|
)
|
|
262
262
|
except (RuntimeError, TypeError, ValueError, AttributeError) as e:
|
|
263
|
-
logging.
|
|
263
|
+
logging.warning(f"save_state_snapshot failed: {e}")
|
|
264
264
|
|
|
265
265
|
def update_window_title(self) -> None:
|
|
266
266
|
"""Update main window title to reflect file path and save status."""
|
|
@@ -144,7 +144,7 @@ class MainInitManager:
|
|
|
144
144
|
try:
|
|
145
145
|
self.host.plugin_manager = PluginManager()
|
|
146
146
|
except (AttributeError, RuntimeError, ValueError) as e:
|
|
147
|
-
logging.
|
|
147
|
+
logging.warning(f"Failed to initialize PluginManager: {e}")
|
|
148
148
|
self.host.plugin_manager = None
|
|
149
149
|
|
|
150
150
|
# Dictionary holding data for plugins that haven't been loaded
|
|
@@ -189,7 +189,7 @@ class MainInitManager:
|
|
|
189
189
|
for atom in warmup_mol.GetAtoms():
|
|
190
190
|
atom.GetNumImplicitHs()
|
|
191
191
|
except (AttributeError, RuntimeError, ValueError) as e:
|
|
192
|
-
logging.
|
|
192
|
+
logging.warning(f"RDKit warm-up failed: {e}")
|
|
193
193
|
|
|
194
194
|
self.host.state_manager.reset_undo_stack()
|
|
195
195
|
self.scene.selectionChanged.connect( # type: ignore[attr-defined]
|
|
@@ -304,9 +304,10 @@ class MainInitManager:
|
|
|
304
304
|
self.host.state_manager.update_window_title()
|
|
305
305
|
return # Success
|
|
306
306
|
except Exception: # plugins have full app access; any failure falls through to the next opener
|
|
307
|
-
logging.
|
|
307
|
+
logging.warning(
|
|
308
308
|
"Plugin opener failed for '%s'",
|
|
309
309
|
opener_info.get("plugin", "Unknown"),
|
|
310
|
+
exc_info=True,
|
|
310
311
|
)
|
|
311
312
|
continue
|
|
312
313
|
|
|
@@ -404,7 +405,7 @@ class MainInitManager:
|
|
|
404
405
|
c.blueF(),
|
|
405
406
|
]
|
|
406
407
|
except (AttributeError, RuntimeError, TypeError, ValueError) as e:
|
|
407
|
-
logging.
|
|
408
|
+
logging.warning(f"Failed to update CPK colors from settings: {e}")
|
|
408
409
|
|
|
409
410
|
def open_settings_dialog(self) -> None:
|
|
410
411
|
"""Open the application settings dialog."""
|
|
@@ -567,7 +568,7 @@ class MainInitManager:
|
|
|
567
568
|
self.settings_dirty = False
|
|
568
569
|
self.host.initial_settings = self.settings.copy()
|
|
569
570
|
except (AttributeError, RuntimeError, ValueError) as e:
|
|
570
|
-
logging.
|
|
571
|
+
logging.warning(f"Error saving settings: {e}")
|
|
571
572
|
|
|
572
573
|
# --- UI Initialization Helpers ---
|
|
573
574
|
def _init_main_layout(self) -> None:
|
|
@@ -125,7 +125,7 @@ class MirrorDialog(QDialog):
|
|
|
125
125
|
# Calculate new chiral tags from 3D coordinates
|
|
126
126
|
Chem.AssignAtomChiralTagsFromStructure(self.mol, confId=0)
|
|
127
127
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
128
|
-
logging.
|
|
128
|
+
logging.warning("Error updating chiral tags", exc_info=True)
|
|
129
129
|
|
|
130
130
|
# Update 3D view (which also draws 3D chiral labels)
|
|
131
131
|
self.main_window.view_3d_manager.draw_molecule_3d(self.mol)
|
|
@@ -141,6 +141,4 @@ class MirrorDialog(QDialog):
|
|
|
141
141
|
)
|
|
142
142
|
|
|
143
143
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
144
|
-
|
|
145
|
-
self, "Error", f"Failed to apply mirror transformation: {str(e)}"
|
|
146
|
-
)
|
|
144
|
+
logging.exception("Failed to apply mirror transformation: %s", e)
|