MoleditPy-linux 4.0.2__py3-none-any.whl → 4.1.1__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/core/mol_geometry.py +24 -8
- moleditpy_linux/main.py +41 -6
- moleditpy_linux/plugins/plugin_interface.py +36 -33
- moleditpy_linux/plugins/plugin_manager_window.py +1 -1
- moleditpy_linux/ui/app_state.py +3 -7
- moleditpy_linux/ui/atom_item.py +50 -19
- moleditpy_linux/ui/bond_item.py +27 -23
- moleditpy_linux/ui/calculation_worker.py +80 -38
- moleditpy_linux/ui/compute_logic.py +6 -6
- moleditpy_linux/ui/constrained_optimization_dialog.py +1 -4
- moleditpy_linux/ui/edit_actions_logic.py +12 -5
- moleditpy_linux/ui/export_logic.py +2 -1
- moleditpy_linux/ui/io_logic.py +5 -5
- moleditpy_linux/ui/molecule_scene.py +22 -15
- moleditpy_linux/ui/periodic_table_dialog.py +8 -6
- moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +47 -0
- moleditpy_linux/ui/settings_tabs/settings_other_tab.py +33 -0
- moleditpy_linux/ui/user_template_dialog.py +1 -1
- moleditpy_linux/ui/view_3d_logic.py +0 -2
- moleditpy_linux/ui/zoomable_view.py +4 -0
- moleditpy_linux/utils/default_settings.py +5 -0
- {moleditpy_linux-4.0.2.dist-info → moleditpy_linux-4.1.1.dist-info}/METADATA +5 -5
- {moleditpy_linux-4.0.2.dist-info → moleditpy_linux-4.1.1.dist-info}/RECORD +27 -27
- {moleditpy_linux-4.0.2.dist-info → moleditpy_linux-4.1.1.dist-info}/WHEEL +0 -0
- {moleditpy_linux-4.0.2.dist-info → moleditpy_linux-4.1.1.dist-info}/entry_points.txt +0 -0
- {moleditpy_linux-4.0.2.dist-info → moleditpy_linux-4.1.1.dist-info}/licenses/LICENSE +0 -0
- {moleditpy_linux-4.0.2.dist-info → moleditpy_linux-4.1.1.dist-info}/top_level.txt +0 -0
|
@@ -13,7 +13,21 @@ DOI: 10.5281/zenodo.17268532
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
import math
|
|
15
15
|
from collections import deque
|
|
16
|
-
from typing import
|
|
16
|
+
from typing import (
|
|
17
|
+
TYPE_CHECKING,
|
|
18
|
+
Any,
|
|
19
|
+
Callable,
|
|
20
|
+
Dict,
|
|
21
|
+
Iterable,
|
|
22
|
+
List,
|
|
23
|
+
Optional,
|
|
24
|
+
Set,
|
|
25
|
+
Tuple,
|
|
26
|
+
Union,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from rdkit import Chem
|
|
17
31
|
|
|
18
32
|
import numpy as np
|
|
19
33
|
|
|
@@ -82,7 +96,7 @@ def calc_angle_deg(
|
|
|
82
96
|
|
|
83
97
|
|
|
84
98
|
def get_connected_group(
|
|
85
|
-
mol:
|
|
99
|
+
mol: Chem.Mol, start_atom: int, exclude: Optional[int] = None
|
|
86
100
|
) -> Set[int]:
|
|
87
101
|
"""Return the set of atom indices reachable from *start_atom*
|
|
88
102
|
without passing through *exclude*.
|
|
@@ -422,7 +436,9 @@ def is_problematic_valence(
|
|
|
422
436
|
|
|
423
437
|
|
|
424
438
|
def inject_ez_stereo_to_mol_block(
|
|
425
|
-
mol_block: str,
|
|
439
|
+
mol_block: str,
|
|
440
|
+
rdkit_mol: Chem.Mol,
|
|
441
|
+
bonds_data: Dict[Tuple[int, int], Dict[str, Any]],
|
|
426
442
|
) -> str:
|
|
427
443
|
"""Generate a modified MOL block with 'M CFG' lines for E/Z stereochemistry.
|
|
428
444
|
|
|
@@ -504,7 +520,7 @@ def identify_valence_problems(
|
|
|
504
520
|
problem_atom_ids = []
|
|
505
521
|
|
|
506
522
|
# Pre-calculate bond orders per atom
|
|
507
|
-
bond_orders: Dict[int,
|
|
523
|
+
bond_orders: Dict[int, float] = {}
|
|
508
524
|
for (id1, id2), bond in bonds_data.items():
|
|
509
525
|
order = bond.get("order", 1)
|
|
510
526
|
bond_orders[id1] = bond_orders.get(id1, 0) + order
|
|
@@ -521,7 +537,7 @@ def identify_valence_problems(
|
|
|
521
537
|
return problem_atom_ids
|
|
522
538
|
|
|
523
539
|
|
|
524
|
-
def optimize_2d_coords(mol:
|
|
540
|
+
def optimize_2d_coords(mol: Chem.Mol) -> Dict[int, Tuple[float, float]]:
|
|
525
541
|
"""Generate 2D coordinates using RDKit and return a map of (x, y) tuples."""
|
|
526
542
|
from rdkit.Chem import AllChem
|
|
527
543
|
|
|
@@ -572,7 +588,7 @@ def resolve_2d_overlaps(
|
|
|
572
588
|
adjacency_list: Dict[int, List[int]],
|
|
573
589
|
overlap_threshold: float = 0.5,
|
|
574
590
|
move_distance: float = 20,
|
|
575
|
-
has_bond_check_func: Optional[
|
|
591
|
+
has_bond_check_func: Optional[Callable[[int, int], bool]] = None,
|
|
576
592
|
) -> List[Tuple[Set[int], Tuple[float, float]]]:
|
|
577
593
|
"""Detect and resolve overlapping atom groups in 2D.
|
|
578
594
|
|
|
@@ -601,13 +617,13 @@ def resolve_2d_overlaps(
|
|
|
601
617
|
# Union-Find for overlap groups
|
|
602
618
|
parent = {aid: aid for aid in atom_ids}
|
|
603
619
|
|
|
604
|
-
def find_set(aid:
|
|
620
|
+
def find_set(aid: int) -> int:
|
|
605
621
|
if parent[aid] == aid:
|
|
606
622
|
return aid
|
|
607
623
|
parent[aid] = find_set(parent[aid])
|
|
608
624
|
return parent[aid]
|
|
609
625
|
|
|
610
|
-
def unite_sets(aid1:
|
|
626
|
+
def unite_sets(aid1: int, aid2: int) -> None:
|
|
611
627
|
root1 = find_set(aid1)
|
|
612
628
|
root2 = find_set(aid2)
|
|
613
629
|
if root1 != root2:
|
moleditpy_linux/main.py
CHANGED
|
@@ -11,10 +11,12 @@ DOI: 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import ctypes
|
|
14
|
-
import
|
|
15
|
-
import argparse
|
|
14
|
+
import json
|
|
16
15
|
import logging
|
|
16
|
+
import logging.handlers
|
|
17
17
|
import os
|
|
18
|
+
import sys
|
|
19
|
+
import argparse
|
|
18
20
|
from typing import Any
|
|
19
21
|
|
|
20
22
|
from .utils.constants import VERSION
|
|
@@ -46,22 +48,55 @@ def _qt_message_handler(mode: QtMsgType, _context: Any, message: str) -> None:
|
|
|
46
48
|
logging.log(_QT_LOG_LEVEL.get(mode, logging.WARNING), "Qt: %s", message)
|
|
47
49
|
|
|
48
50
|
|
|
51
|
+
def _read_startup_log_settings() -> tuple[bool, bool]:
|
|
52
|
+
"""Read log_to_file and log_level_debug from settings.json before Qt starts.
|
|
53
|
+
|
|
54
|
+
Returns (log_to_file, log_level_debug). Falls back to (False, False) on any error.
|
|
55
|
+
"""
|
|
56
|
+
settings_path = os.path.join(os.path.expanduser("~"), ".moleditpy", "settings.json")
|
|
57
|
+
try:
|
|
58
|
+
with open(settings_path, encoding="utf-8") as f:
|
|
59
|
+
data = json.load(f)
|
|
60
|
+
return bool(data.get("log_to_file", False)), bool(
|
|
61
|
+
data.get("log_level_debug", False)
|
|
62
|
+
)
|
|
63
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
64
|
+
return False, False
|
|
65
|
+
|
|
66
|
+
|
|
49
67
|
def setup_logging() -> None:
|
|
50
68
|
"""Configure root logger and install a global unhandled-exception handler."""
|
|
69
|
+
log_to_file, log_level_debug = _read_startup_log_settings()
|
|
70
|
+
level = logging.DEBUG if log_level_debug else logging.INFO
|
|
71
|
+
fmt = "%(asctime)s [%(levelname)s] %(name)s (%(pathname)s:%(lineno)d): %(message)s"
|
|
72
|
+
|
|
51
73
|
logging.basicConfig(
|
|
52
|
-
level=
|
|
53
|
-
format=
|
|
74
|
+
level=level,
|
|
75
|
+
format=fmt,
|
|
54
76
|
stream=sys.stdout,
|
|
55
77
|
force=True,
|
|
56
78
|
)
|
|
57
79
|
|
|
80
|
+
if log_to_file:
|
|
81
|
+
log_dir = os.path.join(os.path.expanduser("~"), ".moleditpy")
|
|
82
|
+
try:
|
|
83
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
84
|
+
log_path = os.path.join(log_dir, "moleditpy_linux.log")
|
|
85
|
+
fh = logging.handlers.RotatingFileHandler(
|
|
86
|
+
log_path, maxBytes=1_048_576, backupCount=3, encoding="utf-8"
|
|
87
|
+
)
|
|
88
|
+
fh.setLevel(level)
|
|
89
|
+
fh.setFormatter(logging.Formatter(fmt))
|
|
90
|
+
logging.getLogger().addHandler(fh)
|
|
91
|
+
logging.info("File logging enabled: %s", log_path)
|
|
92
|
+
except OSError as e:
|
|
93
|
+
logging.warning("Could not open log file: %s", e)
|
|
94
|
+
|
|
58
95
|
def handle_exception(exc_type: Any, exc_value: Any, exc_traceback: Any) -> None:
|
|
59
96
|
"""Log unhandled exceptions using the configured logging system."""
|
|
60
97
|
if issubclass(exc_type, KeyboardInterrupt):
|
|
61
|
-
# Allow keyboard interrupt to exit normally
|
|
62
98
|
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
63
99
|
return
|
|
64
|
-
|
|
65
100
|
logging.error(
|
|
66
101
|
"Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
|
|
67
102
|
)
|
|
@@ -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
|
from typing import Any, Callable, List, Optional, Union
|
|
14
15
|
|
|
15
16
|
|
|
@@ -173,10 +174,11 @@ class PluginContext:
|
|
|
173
174
|
"""Switch the application UI layout to 3D viewer mode (Public API)."""
|
|
174
175
|
mw = self.get_main_window()
|
|
175
176
|
if mw is not None and hasattr(mw, "ui_manager"):
|
|
176
|
-
|
|
177
|
-
mw.ui_manager
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
fn = getattr(mw.ui_manager, "enter_3d_viewer_mode", None) or getattr(
|
|
178
|
+
mw.ui_manager, "enter_3d_viewer_ui_mode", None
|
|
179
|
+
)
|
|
180
|
+
if fn:
|
|
181
|
+
fn()
|
|
180
182
|
|
|
181
183
|
def enter_3d_mode(self) -> None:
|
|
182
184
|
"""Switch UI layout to 3D viewer mode. Alias for enter_3d_viewer_mode."""
|
|
@@ -240,16 +242,11 @@ class PluginContext:
|
|
|
240
242
|
"""Force the 3D window to redraw using the current molecule."""
|
|
241
243
|
mw = self.get_main_window()
|
|
242
244
|
if mw and hasattr(mw, "view_3d_manager"):
|
|
243
|
-
mol =
|
|
245
|
+
mol = mw.view_3d_manager.current_mol
|
|
244
246
|
if mol:
|
|
245
247
|
mw.view_3d_manager.draw_molecule_3d(mol)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
hasattr(mw.view_3d_manager, "plotter")
|
|
250
|
-
and mw.view_3d_manager.plotter
|
|
251
|
-
):
|
|
252
|
-
mw.view_3d_manager.plotter.render()
|
|
248
|
+
elif mw.view_3d_manager.plotter:
|
|
249
|
+
mw.view_3d_manager.plotter.render()
|
|
253
250
|
|
|
254
251
|
def reset_3d_camera(self) -> None:
|
|
255
252
|
"""Zoom in and re-center the 3D viewport to fit the current molecule."""
|
|
@@ -370,7 +367,7 @@ class PluginContext:
|
|
|
370
367
|
default: Value to return if the setting is not found.
|
|
371
368
|
"""
|
|
372
369
|
mw = self.get_main_window()
|
|
373
|
-
if mw and hasattr(mw, "init_manager")
|
|
370
|
+
if mw and hasattr(mw, "init_manager"):
|
|
374
371
|
namespaced = f"plugin.{self._plugin_name}.{key}"
|
|
375
372
|
return mw.init_manager.settings.get(namespaced, default)
|
|
376
373
|
return default
|
|
@@ -387,11 +384,10 @@ class PluginContext:
|
|
|
387
384
|
value: Value to store (must be JSON-serializable).
|
|
388
385
|
"""
|
|
389
386
|
mw = self.get_main_window()
|
|
390
|
-
if mw and hasattr(mw, "init_manager")
|
|
387
|
+
if mw and hasattr(mw, "init_manager"):
|
|
391
388
|
namespaced = f"plugin.{self._plugin_name}.{key}"
|
|
392
389
|
mw.init_manager.settings[namespaced] = value
|
|
393
|
-
|
|
394
|
-
mw.init_manager.settings_dirty = True
|
|
390
|
+
mw.init_manager.settings_dirty = True
|
|
395
391
|
|
|
396
392
|
def mark_project_modified(self) -> None:
|
|
397
393
|
"""Mark the current project as having unsaved changes and update the window title."""
|
|
@@ -399,8 +395,9 @@ class PluginContext:
|
|
|
399
395
|
if mw and hasattr(mw, "state_manager"):
|
|
400
396
|
try:
|
|
401
397
|
mw.state_manager.has_unsaved_changes = True
|
|
402
|
-
|
|
403
|
-
|
|
398
|
+
fn = getattr(mw.state_manager, "update_window_title", None)
|
|
399
|
+
if fn:
|
|
400
|
+
fn()
|
|
404
401
|
except Exception:
|
|
405
402
|
pass
|
|
406
403
|
|
|
@@ -414,14 +411,16 @@ class PluginContext:
|
|
|
414
411
|
if mw is None:
|
|
415
412
|
return
|
|
416
413
|
if hasattr(mw, "state_manager"):
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
):
|
|
424
|
-
mw.edit_actions_manager
|
|
414
|
+
fn = getattr(mw.state_manager, "update_realtime_info", None)
|
|
415
|
+
if fn:
|
|
416
|
+
fn()
|
|
417
|
+
fn = getattr(mw.state_manager, "update_window_title", None)
|
|
418
|
+
if fn:
|
|
419
|
+
fn()
|
|
420
|
+
if hasattr(mw, "edit_actions_manager"):
|
|
421
|
+
fn = getattr(mw.edit_actions_manager, "update_undo_redo_actions", None)
|
|
422
|
+
if fn:
|
|
423
|
+
fn()
|
|
425
424
|
|
|
426
425
|
def fit_3d_view(self) -> None:
|
|
427
426
|
"""Zoom and re-center the 3D viewport to fit the current molecule."""
|
|
@@ -489,8 +488,10 @@ class PluginContext:
|
|
|
489
488
|
mw = self.get_main_window()
|
|
490
489
|
if mw and hasattr(mw, "init_manager"):
|
|
491
490
|
scene = getattr(mw.init_manager, "scene", None)
|
|
492
|
-
if scene is not None
|
|
493
|
-
scene
|
|
491
|
+
if scene is not None:
|
|
492
|
+
fn = getattr(scene, "update_all_items", None)
|
|
493
|
+
if fn:
|
|
494
|
+
fn()
|
|
494
495
|
|
|
495
496
|
def load_from_smiles(self, smiles: str) -> None:
|
|
496
497
|
"""Add a molecule from a SMILES string to the 2D editor."""
|
|
@@ -518,6 +519,7 @@ class PluginContext:
|
|
|
518
519
|
|
|
519
520
|
return "\n".join(xyz_lines)
|
|
520
521
|
except Exception:
|
|
522
|
+
logging.debug("to_xyz_block failed", exc_info=True)
|
|
521
523
|
return None
|
|
522
524
|
|
|
523
525
|
|
|
@@ -541,8 +543,8 @@ class Plugin3DController:
|
|
|
541
543
|
v3d = self._get_v3d()
|
|
542
544
|
if v3d:
|
|
543
545
|
v3d.update_atom_color_override(atom_index, color_hex)
|
|
544
|
-
if
|
|
545
|
-
|
|
546
|
+
if v3d.plotter:
|
|
547
|
+
v3d.plotter.render()
|
|
546
548
|
|
|
547
549
|
def set_bond_color(self, bond_index: int, color_hex: str) -> None:
|
|
548
550
|
"""
|
|
@@ -555,8 +557,8 @@ class Plugin3DController:
|
|
|
555
557
|
v3d = self._get_v3d()
|
|
556
558
|
if v3d:
|
|
557
559
|
v3d.update_bond_color_override(bond_index, color_hex)
|
|
558
|
-
if
|
|
559
|
-
|
|
560
|
+
if v3d.plotter:
|
|
561
|
+
v3d.plotter.render()
|
|
560
562
|
|
|
561
563
|
def set_bond_color_by_atoms(
|
|
562
564
|
self, atom_idx1: int, atom_idx2: int, color_hex: str
|
|
@@ -569,7 +571,8 @@ class Plugin3DController:
|
|
|
569
571
|
atom_idx2: Second RDKit atom index.
|
|
570
572
|
color_hex: Hex string e.g., "#00FF00".
|
|
571
573
|
"""
|
|
572
|
-
|
|
574
|
+
v3d = self._get_v3d()
|
|
575
|
+
mol = v3d.current_mol if v3d else None
|
|
573
576
|
if not mol:
|
|
574
577
|
return
|
|
575
578
|
|
|
@@ -213,7 +213,7 @@ class PluginManagerWindow(QDialog):
|
|
|
213
213
|
"Success",
|
|
214
214
|
f"Removed '{plugin.get('name', 'Unknown')}'.",
|
|
215
215
|
)
|
|
216
|
-
except
|
|
216
|
+
except OSError as e:
|
|
217
217
|
QMessageBox.critical(
|
|
218
218
|
self, "Error", f"Failed to delete plugin: {e}"
|
|
219
219
|
)
|
moleditpy_linux/ui/app_state.py
CHANGED
|
@@ -542,13 +542,9 @@ class StateManager:
|
|
|
542
542
|
# 3D viewer mode
|
|
543
543
|
is_3d_mode = json_data.get("is_3d_viewer_mode", False)
|
|
544
544
|
# Restore last successful optimization method if present in file
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
self.host.set_last_successful_optimization_method(method)
|
|
549
|
-
except (AttributeError, RuntimeError, TypeError):
|
|
550
|
-
# Safe defensive fallback catching AttributeError, RuntimeError, TypeError
|
|
551
|
-
pass
|
|
545
|
+
method = json_data.get("last_successful_optimization_method", None)
|
|
546
|
+
with contextlib.suppress(AttributeError, RuntimeError, TypeError):
|
|
547
|
+
self.host.set_last_successful_optimization_method(method)
|
|
552
548
|
|
|
553
549
|
# Plugin State Restoration (Phase 3)
|
|
554
550
|
self._preserved_plugin_data = {} # Reset preserved data on new load
|
moleditpy_linux/ui/atom_item.py
CHANGED
|
@@ -11,7 +11,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
|
-
from typing import Any, List, Optional
|
|
14
|
+
from typing import TYPE_CHECKING, Any, List, Optional
|
|
15
15
|
|
|
16
16
|
from PyQt6.QtCore import QPointF, QRectF, Qt
|
|
17
17
|
from PyQt6.QtGui import (
|
|
@@ -23,7 +23,12 @@ from PyQt6.QtGui import (
|
|
|
23
23
|
QPainterPath,
|
|
24
24
|
QPen,
|
|
25
25
|
)
|
|
26
|
-
from PyQt6.QtWidgets import
|
|
26
|
+
from PyQt6.QtWidgets import (
|
|
27
|
+
QGraphicsItem,
|
|
28
|
+
QGraphicsSceneHoverEvent,
|
|
29
|
+
QStyleOptionGraphicsItem,
|
|
30
|
+
QWidget,
|
|
31
|
+
)
|
|
27
32
|
|
|
28
33
|
from ..utils.constants import (
|
|
29
34
|
ATOM_RADIUS,
|
|
@@ -34,6 +39,9 @@ from ..utils.constants import (
|
|
|
34
39
|
)
|
|
35
40
|
from ..utils.sip_isdeleted_safe import sip_isdeleted_safe
|
|
36
41
|
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from .bond_item import BondItem
|
|
44
|
+
|
|
37
45
|
|
|
38
46
|
class AtomItem(QGraphicsItem):
|
|
39
47
|
"""2D scene item representing a single atom in the molecule editor."""
|
|
@@ -46,7 +54,7 @@ class AtomItem(QGraphicsItem):
|
|
|
46
54
|
self.symbol: str = symbol
|
|
47
55
|
self.charge: int = charge
|
|
48
56
|
self.radical: int = radical
|
|
49
|
-
self.bonds: List[
|
|
57
|
+
self.bonds: List[BondItem] = []
|
|
50
58
|
self.chiral_label: Optional[str] = None
|
|
51
59
|
|
|
52
60
|
self.setPos(pos)
|
|
@@ -67,16 +75,24 @@ class AtomItem(QGraphicsItem):
|
|
|
67
75
|
"""Refresh font, color, and visibility based on current scene settings."""
|
|
68
76
|
if sip_isdeleted_safe(self):
|
|
69
77
|
return
|
|
70
|
-
# Allow updating font preference dynamically
|
|
71
78
|
font_size = 20
|
|
72
79
|
font_family = FONT_FAMILY
|
|
80
|
+
font_bold = True
|
|
81
|
+
font_italic = False
|
|
82
|
+
font_underline = False
|
|
73
83
|
|
|
74
84
|
scene = self.scene()
|
|
75
85
|
if scene is not None and hasattr(scene, "get_setting"):
|
|
76
86
|
font_size = scene.get_setting("atom_font_size_2d", 20)
|
|
77
87
|
font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
font_bold = scene.get_setting("atom_font_bold_2d", True)
|
|
89
|
+
font_italic = scene.get_setting("atom_font_italic_2d", False)
|
|
90
|
+
font_underline = scene.get_setting("atom_font_underline_2d", False)
|
|
91
|
+
|
|
92
|
+
weight = QFont.Weight.Bold if font_bold else QFont.Weight.Normal
|
|
93
|
+
self.font = QFont(font_family, font_size, weight)
|
|
94
|
+
self.font.setItalic(font_italic)
|
|
95
|
+
self.font.setUnderline(font_underline)
|
|
80
96
|
self.prepareGeometryChange()
|
|
81
97
|
|
|
82
98
|
self.is_visible = not (
|
|
@@ -89,16 +105,23 @@ class AtomItem(QGraphicsItem):
|
|
|
89
105
|
|
|
90
106
|
def boundingRect(self) -> QRectF:
|
|
91
107
|
"""Calculate the bounding rectangle for the atom item."""
|
|
92
|
-
# --- Calculate text position and size using logic matching paint() ---
|
|
93
|
-
# Get dynamic font size and family
|
|
94
108
|
font_size = 20
|
|
95
109
|
font_family = FONT_FAMILY
|
|
110
|
+
font_bold = True
|
|
111
|
+
font_italic = False
|
|
112
|
+
font_underline = False
|
|
96
113
|
scene = self.scene()
|
|
97
114
|
if scene is not None and hasattr(scene, "get_setting"):
|
|
98
115
|
font_size = scene.get_setting("atom_font_size_2d", 20)
|
|
99
116
|
font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
font_bold = scene.get_setting("atom_font_bold_2d", True)
|
|
118
|
+
font_italic = scene.get_setting("atom_font_italic_2d", False)
|
|
119
|
+
font_underline = scene.get_setting("atom_font_underline_2d", False)
|
|
120
|
+
|
|
121
|
+
weight = QFont.Weight.Bold if font_bold else QFont.Weight.Normal
|
|
122
|
+
font = QFont(font_family, font_size, weight)
|
|
123
|
+
font.setItalic(font_italic)
|
|
124
|
+
font.setUnderline(font_underline)
|
|
102
125
|
fm = QFontMetricsF(font)
|
|
103
126
|
|
|
104
127
|
hydrogen_part = ""
|
|
@@ -207,13 +230,21 @@ class AtomItem(QGraphicsItem):
|
|
|
207
230
|
|
|
208
231
|
font_size = 20
|
|
209
232
|
font_family = FONT_FAMILY
|
|
233
|
+
font_bold = True
|
|
234
|
+
font_italic = False
|
|
235
|
+
font_underline = False
|
|
210
236
|
scene = self.scene()
|
|
211
|
-
if scene is not None:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
if scene is not None and hasattr(scene, "get_setting"):
|
|
238
|
+
font_size = scene.get_setting("atom_font_size_2d", 20)
|
|
239
|
+
font_family = scene.get_setting("atom_font_family_2d", FONT_FAMILY)
|
|
240
|
+
font_bold = scene.get_setting("atom_font_bold_2d", True)
|
|
241
|
+
font_italic = scene.get_setting("atom_font_italic_2d", False)
|
|
242
|
+
font_underline = scene.get_setting("atom_font_underline_2d", False)
|
|
243
|
+
|
|
244
|
+
weight = QFont.Weight.Bold if font_bold else QFont.Weight.Normal
|
|
245
|
+
font = QFont(font_family, font_size, weight)
|
|
246
|
+
font.setItalic(font_italic)
|
|
247
|
+
font.setUnderline(font_underline)
|
|
217
248
|
fm = QFontMetricsF(font)
|
|
218
249
|
|
|
219
250
|
hydrogen_part = ""
|
|
@@ -292,7 +323,7 @@ class AtomItem(QGraphicsItem):
|
|
|
292
323
|
def paint(
|
|
293
324
|
self,
|
|
294
325
|
painter: Optional[QPainter],
|
|
295
|
-
option:
|
|
326
|
+
option: QStyleOptionGraphicsItem,
|
|
296
327
|
widget: Optional[QWidget] = None,
|
|
297
328
|
) -> None:
|
|
298
329
|
"""Paint the atom symbol and its associated labels (charge, radical)."""
|
|
@@ -484,14 +515,14 @@ class AtomItem(QGraphicsItem):
|
|
|
484
515
|
self.update_style()
|
|
485
516
|
return res
|
|
486
517
|
|
|
487
|
-
def hoverEnterEvent(self, event:
|
|
518
|
+
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent) -> None:
|
|
488
519
|
"""Highlight the atom on mouse hover."""
|
|
489
520
|
# Enable highlight on hover regardless of scene mode
|
|
490
521
|
self.hovered = True
|
|
491
522
|
self.update()
|
|
492
523
|
super().hoverEnterEvent(event)
|
|
493
524
|
|
|
494
|
-
def hoverLeaveEvent(self, event:
|
|
525
|
+
def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent) -> None:
|
|
495
526
|
"""Remove hover highlight when the mouse leaves."""
|
|
496
527
|
if self.hovered:
|
|
497
528
|
self.hovered = False
|
moleditpy_linux/ui/bond_item.py
CHANGED
|
@@ -13,7 +13,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
import math
|
|
15
15
|
import logging
|
|
16
|
-
from typing import
|
|
16
|
+
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
|
17
17
|
|
|
18
18
|
from PyQt6.QtCore import QLineF, QPointF, QRectF, Qt
|
|
19
19
|
from PyQt6.QtGui import (
|
|
@@ -27,7 +27,13 @@ from PyQt6.QtGui import (
|
|
|
27
27
|
QPen,
|
|
28
28
|
QPolygonF,
|
|
29
29
|
)
|
|
30
|
-
from PyQt6.QtWidgets import
|
|
30
|
+
from PyQt6.QtWidgets import (
|
|
31
|
+
QGraphicsItem,
|
|
32
|
+
QGraphicsScene,
|
|
33
|
+
QGraphicsSceneHoverEvent,
|
|
34
|
+
QStyleOptionGraphicsItem,
|
|
35
|
+
QWidget,
|
|
36
|
+
)
|
|
31
37
|
|
|
32
38
|
from ..utils.constants import (
|
|
33
39
|
DESIRED_BOND_PIXEL_WIDTH,
|
|
@@ -39,6 +45,9 @@ from ..utils.constants import (
|
|
|
39
45
|
HOVER_PEN_WIDTH,
|
|
40
46
|
)
|
|
41
47
|
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from .atom_item import AtomItem
|
|
50
|
+
|
|
42
51
|
|
|
43
52
|
class BondItem(QGraphicsItem):
|
|
44
53
|
"""Visual representation of a molecular bond in the 2D scene."""
|
|
@@ -138,14 +147,18 @@ class BondItem(QGraphicsItem):
|
|
|
138
147
|
return min(0.9, 0.8 + 0.1 * openness)
|
|
139
148
|
|
|
140
149
|
def __init__(
|
|
141
|
-
self,
|
|
150
|
+
self,
|
|
151
|
+
atom1_item: AtomItem,
|
|
152
|
+
atom2_item: AtomItem,
|
|
153
|
+
order: int = 1,
|
|
154
|
+
stereo: int = 0,
|
|
142
155
|
) -> None:
|
|
143
156
|
super().__init__()
|
|
144
157
|
# Validate input parameters
|
|
145
158
|
if atom1_item is None or atom2_item is None:
|
|
146
159
|
raise ValueError("BondItem requires non-None atom items")
|
|
147
|
-
self.atom1:
|
|
148
|
-
self.atom2:
|
|
160
|
+
self.atom1: Optional[AtomItem] = atom1_item
|
|
161
|
+
self.atom2: Optional[AtomItem] = atom2_item
|
|
149
162
|
self.order: int = order
|
|
150
163
|
self.stereo: int = stereo
|
|
151
164
|
|
|
@@ -314,7 +327,7 @@ class BondItem(QGraphicsItem):
|
|
|
314
327
|
def paint(
|
|
315
328
|
self,
|
|
316
329
|
painter: Optional[QPainter],
|
|
317
|
-
option:
|
|
330
|
+
option: QStyleOptionGraphicsItem,
|
|
318
331
|
widget: Optional[QWidget] = None,
|
|
319
332
|
) -> None:
|
|
320
333
|
"""Render the bond as single, double, triple, wedge, or dashed line."""
|
|
@@ -506,21 +519,12 @@ class BondItem(QGraphicsItem):
|
|
|
506
519
|
# --- Label Settings ---
|
|
507
520
|
font_size = 20
|
|
508
521
|
font_family = FONT_FAMILY
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
)
|
|
516
|
-
font_family = win.settings.get(
|
|
517
|
-
"atom_font_family_2d", FONT_FAMILY
|
|
518
|
-
)
|
|
519
|
-
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
520
|
-
# Silent failure for non-critical 2D atom font setting
|
|
521
|
-
# If we can't get custom settings, we just use defaults.
|
|
522
|
-
# Safe defensive fallback catching AttributeError, RuntimeError, TypeError, ValueError
|
|
523
|
-
pass
|
|
522
|
+
_sc = self.scene()
|
|
523
|
+
if _sc is not None and hasattr(_sc, "get_setting"):
|
|
524
|
+
font_size = _sc.get_setting("atom_font_size_2d", 20)
|
|
525
|
+
font_family = _sc.get_setting(
|
|
526
|
+
"atom_font_family_2d", FONT_FAMILY
|
|
527
|
+
)
|
|
524
528
|
|
|
525
529
|
font = QFont(font_family, font_size, FONT_WEIGHT_BOLD)
|
|
526
530
|
font.setItalic(True)
|
|
@@ -598,7 +602,7 @@ class BondItem(QGraphicsItem):
|
|
|
598
602
|
logging.exception("Error updating bond position")
|
|
599
603
|
# Continue without crashing
|
|
600
604
|
|
|
601
|
-
def hoverEnterEvent(self, event:
|
|
605
|
+
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent) -> None:
|
|
602
606
|
"""Highlight the bond on mouse hover."""
|
|
603
607
|
self.hovered = True
|
|
604
608
|
self.update()
|
|
@@ -606,7 +610,7 @@ class BondItem(QGraphicsItem):
|
|
|
606
610
|
self.scene().set_hovered_item(self)
|
|
607
611
|
super().hoverEnterEvent(event)
|
|
608
612
|
|
|
609
|
-
def hoverLeaveEvent(self, event:
|
|
613
|
+
def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent) -> None:
|
|
610
614
|
"""Remove hover highlight when the mouse leaves."""
|
|
611
615
|
if self.hovered:
|
|
612
616
|
self.hovered = False
|