MoleditPy-linux 4.0.1__tar.gz → 4.1.0__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.
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/PKG-INFO +1 -1
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/pyproject.toml +1 -1
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/core/mol_geometry.py +24 -8
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/core/molecular_data.py +9 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/main.py +65 -12
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/plugins/plugin_interface.py +36 -33
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/plugins/plugin_manager.py +14 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/plugins/plugin_manager_window.py +11 -1
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/about_dialog.py +3 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/analysis_window.py +4 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/angle_dialog.py +3 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/app_state.py +9 -7
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/atom_item.py +57 -19
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/bond_item.py +33 -23
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/bond_length_dialog.py +3 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/calculation_worker.py +22 -26
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/color_settings_dialog.py +5 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/compute_logic.py +6 -3
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +13 -4
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/custom_interactor_style.py +2 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/custom_qt_interactor.py +2 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/dihedral_dialog.py +3 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/edit_actions_logic.py +19 -5
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/export_logic.py +4 -1
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/io_logic.py +7 -5
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/main_window.py +2 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/main_window_init.py +5 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/mirror_dialog.py +1 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/molecular_scene_handler.py +6 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/molecule_scene.py +29 -15
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/move_group_dialog.py +4 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/periodic_table_dialog.py +11 -6
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/planarize_dialog.py +5 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/settings_dialog.py +8 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +49 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +4 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +35 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/template_preview_item.py +8 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/ui_manager.py +6 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/user_template_dialog.py +2 -1
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/view_3d_logic.py +0 -2
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/zoomable_view.py +4 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/utils/default_settings.py +5 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/LICENSE +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/README.md +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/setup.cfg +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/atom_picking.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/string_importers.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/utils/constants.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/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.0
|
|
3
|
+
Version: 4.1.0
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.1.0
|
|
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
|
|
@@ -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:
|
|
@@ -22,13 +22,17 @@ class PointTuple(tuple):
|
|
|
22
22
|
"""Backward-compatible tuple that allows .x() and .y() access like QPointF."""
|
|
23
23
|
|
|
24
24
|
def x(self) -> float:
|
|
25
|
+
"""Return the x coordinate as a float."""
|
|
25
26
|
return float(self[0])
|
|
26
27
|
|
|
27
28
|
def y(self) -> float:
|
|
29
|
+
"""Return the y coordinate as a float."""
|
|
28
30
|
return float(self[1])
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class MolecularData:
|
|
34
|
+
"""In-memory graph of atoms and bonds, independent of any UI framework."""
|
|
35
|
+
|
|
32
36
|
atoms: Dict[int, Dict[str, Any]]
|
|
33
37
|
bonds: Dict[Tuple[int, int], Dict[str, Any]]
|
|
34
38
|
adjacency_list: Dict[int, List[int]]
|
|
@@ -47,6 +51,7 @@ class MolecularData:
|
|
|
47
51
|
charge: int = 0,
|
|
48
52
|
radical: int = 0,
|
|
49
53
|
) -> int:
|
|
54
|
+
"""Add a new atom and return its auto-assigned integer ID."""
|
|
50
55
|
atom_id = self.next_atom_id
|
|
51
56
|
# Internalize position as raw floats to decouple from UI types (QPointF)
|
|
52
57
|
if hasattr(pos, "x") and hasattr(pos, "y"):
|
|
@@ -77,6 +82,7 @@ class MolecularData:
|
|
|
77
82
|
def add_bond(
|
|
78
83
|
self, id1: int, id2: int, order: Union[int, float] = 1, stereo: int = 0
|
|
79
84
|
) -> Tuple[Tuple[int, int], str]:
|
|
85
|
+
"""Add or update a bond between two atoms; return its canonical key and 'created'/'updated'."""
|
|
80
86
|
# For stereo bonds, do not sort because ID order determines direction.
|
|
81
87
|
# For non-stereo bonds, sort to normalize the key.
|
|
82
88
|
if stereo == 0:
|
|
@@ -100,6 +106,7 @@ class MolecularData:
|
|
|
100
106
|
return (id1, id2), "created"
|
|
101
107
|
|
|
102
108
|
def remove_atom(self, atom_id: int) -> None:
|
|
109
|
+
"""Remove an atom and all bonds involving it from the data model."""
|
|
103
110
|
if atom_id in self.atoms:
|
|
104
111
|
# Safely get neighbors before deleting the atom's own entry
|
|
105
112
|
neighbors = self.adjacency_list.get(atom_id, [])
|
|
@@ -123,6 +130,7 @@ class MolecularData:
|
|
|
123
130
|
self.bonds.pop(key, None)
|
|
124
131
|
|
|
125
132
|
def remove_bond(self, id1: int, id2: int) -> None:
|
|
133
|
+
"""Remove the bond between two atoms, handling stereo and non-stereo key variants."""
|
|
126
134
|
# Look for directional stereo bonds (forward/reverse) and normalized non-stereo bond keys.
|
|
127
135
|
key_to_remove = None
|
|
128
136
|
if (id1, id2) in self.bonds:
|
|
@@ -318,6 +326,7 @@ class MolecularData:
|
|
|
318
326
|
return final_mol
|
|
319
327
|
|
|
320
328
|
def to_mol_block(self) -> Optional[str]:
|
|
329
|
+
"""Serialize the molecule to an MDL MOL block string, or None on failure."""
|
|
321
330
|
mol = self.to_rdkit_mol()
|
|
322
331
|
if mol:
|
|
323
332
|
try:
|
|
@@ -11,36 +11,92 @@ 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
|
|
21
23
|
|
|
22
24
|
# VERSION is resolved above (before Qt) so --version works without launching the app.
|
|
23
25
|
|
|
26
|
+
from PyQt6.QtCore import QtMsgType, qInstallMessageHandler
|
|
24
27
|
from PyQt6.QtWidgets import QApplication
|
|
25
28
|
|
|
26
29
|
from .ui.main_window import MainWindow
|
|
27
30
|
|
|
31
|
+
_QT_LOG_LEVEL = {
|
|
32
|
+
QtMsgType.QtDebugMsg: logging.DEBUG,
|
|
33
|
+
QtMsgType.QtInfoMsg: logging.INFO,
|
|
34
|
+
QtMsgType.QtWarningMsg: logging.WARNING,
|
|
35
|
+
QtMsgType.QtCriticalMsg: logging.ERROR,
|
|
36
|
+
QtMsgType.QtFatalMsg: logging.CRITICAL,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_DOWNGRADED_QT_PATTERNS = ("Retrying to obtain clipboard",)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _qt_message_handler(mode: QtMsgType, _context: Any, message: str) -> None:
|
|
43
|
+
"""Route Qt log messages to Python logging, downgrading known noisy warnings."""
|
|
44
|
+
for pattern in _DOWNGRADED_QT_PATTERNS:
|
|
45
|
+
if pattern in message:
|
|
46
|
+
logging.debug("Qt: %s", message)
|
|
47
|
+
return
|
|
48
|
+
logging.log(_QT_LOG_LEVEL.get(mode, logging.WARNING), "Qt: %s", message)
|
|
49
|
+
|
|
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
|
+
|
|
28
66
|
|
|
29
67
|
def setup_logging() -> None:
|
|
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
|
+
|
|
30
73
|
logging.basicConfig(
|
|
31
|
-
level=
|
|
32
|
-
format=
|
|
74
|
+
level=level,
|
|
75
|
+
format=fmt,
|
|
33
76
|
stream=sys.stdout,
|
|
34
77
|
force=True,
|
|
35
78
|
)
|
|
36
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
|
+
|
|
37
95
|
def handle_exception(exc_type: Any, exc_value: Any, exc_traceback: Any) -> None:
|
|
38
96
|
"""Log unhandled exceptions using the configured logging system."""
|
|
39
97
|
if issubclass(exc_type, KeyboardInterrupt):
|
|
40
|
-
# Allow keyboard interrupt to exit normally
|
|
41
98
|
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
42
99
|
return
|
|
43
|
-
|
|
44
100
|
logging.error(
|
|
45
101
|
"Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
|
|
46
102
|
)
|
|
@@ -49,12 +105,11 @@ def setup_logging() -> None:
|
|
|
49
105
|
|
|
50
106
|
|
|
51
107
|
def main() -> None:
|
|
52
|
-
|
|
108
|
+
"""Parse CLI arguments, configure logging, and launch the GUI."""
|
|
53
109
|
setup_logging()
|
|
54
110
|
|
|
55
|
-
# --- Additional handling for Windows taskbar icon ---
|
|
56
111
|
if sys.platform == "win32":
|
|
57
|
-
myappid = "hyoko.moleditpy_linux.1.0"
|
|
112
|
+
myappid = "hyoko.moleditpy_linux.1.0"
|
|
58
113
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
|
59
114
|
|
|
60
115
|
parser = argparse.ArgumentParser(
|
|
@@ -80,7 +135,6 @@ def main() -> None:
|
|
|
80
135
|
# parse_known_args so Qt's own argv flags (e.g. -platform) are passed through
|
|
81
136
|
args, remaining = parser.parse_known_args()
|
|
82
137
|
|
|
83
|
-
# --- Headless Plugin Installation ---
|
|
84
138
|
if args.install_plugin:
|
|
85
139
|
plugin_path = os.path.abspath(args.install_plugin)
|
|
86
140
|
if not os.path.exists(plugin_path):
|
|
@@ -95,7 +149,6 @@ def main() -> None:
|
|
|
95
149
|
pm = PluginManager()
|
|
96
150
|
sha256 = pm.compute_sha256(plugin_path)
|
|
97
151
|
|
|
98
|
-
# Extract metadata
|
|
99
152
|
metadata_file = plugin_path
|
|
100
153
|
if os.path.isdir(plugin_path):
|
|
101
154
|
init_py = os.path.join(plugin_path, "__init__.py")
|
|
@@ -136,10 +189,10 @@ def main() -> None:
|
|
|
136
189
|
sys.exit(0)
|
|
137
190
|
|
|
138
191
|
app = QApplication([sys.argv[0]] + remaining)
|
|
192
|
+
qInstallMessageHandler(_qt_message_handler)
|
|
139
193
|
window = MainWindow(initial_file=args.file, safe_mode=args.safe)
|
|
140
194
|
window.show()
|
|
141
195
|
|
|
142
|
-
# Force Windows to refresh taskbar/titlebar icon after event loop starts
|
|
143
196
|
if sys.platform == "win32":
|
|
144
197
|
try:
|
|
145
198
|
from PyQt6.QtCore import QTimer
|
{moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
@@ -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
|
|
{moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
@@ -29,6 +29,8 @@ from .plugin_interface import PluginContext
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class PluginManager:
|
|
32
|
+
"""Discovers, loads, and manages lifecycle of all installed plugins."""
|
|
33
|
+
|
|
32
34
|
def compute_sha256(self, path: str) -> str:
|
|
33
35
|
"""Computes SHA-256 for a file or a directory (concatenated hashes of all files)."""
|
|
34
36
|
if os.path.isfile(path):
|
|
@@ -98,9 +100,11 @@ class PluginManager:
|
|
|
98
100
|
] = {} # Map of plugin_name -> {window_id -> window}
|
|
99
101
|
|
|
100
102
|
def get_main_window(self) -> Any:
|
|
103
|
+
"""Return the current main window reference."""
|
|
101
104
|
return self.main_window
|
|
102
105
|
|
|
103
106
|
def set_main_window(self, mw: Any) -> None:
|
|
107
|
+
"""Set the main window reference."""
|
|
104
108
|
self.main_window = mw
|
|
105
109
|
|
|
106
110
|
def ensure_plugin_dir(self) -> None:
|
|
@@ -420,6 +424,7 @@ class PluginManager:
|
|
|
420
424
|
icon: str,
|
|
421
425
|
shortcut: str,
|
|
422
426
|
) -> None:
|
|
427
|
+
"""Register a plugin menu action with its path, callback, and display metadata."""
|
|
423
428
|
self.menu_actions.append(
|
|
424
429
|
{
|
|
425
430
|
"plugin": plugin_name,
|
|
@@ -434,6 +439,7 @@ class PluginManager:
|
|
|
434
439
|
def register_toolbar_action(
|
|
435
440
|
self, plugin_name: str, callback: Callable, text: str, icon: str, tooltip: str
|
|
436
441
|
) -> None:
|
|
442
|
+
"""Register a plugin toolbar button with its callback and display metadata."""
|
|
437
443
|
self.toolbar_actions.append(
|
|
438
444
|
{
|
|
439
445
|
"plugin": plugin_name,
|
|
@@ -447,6 +453,7 @@ class PluginManager:
|
|
|
447
453
|
def register_drop_handler(
|
|
448
454
|
self, plugin_name: str, callback: Callable, priority: int
|
|
449
455
|
) -> None:
|
|
456
|
+
"""Register a drag-and-drop handler with a given priority."""
|
|
450
457
|
self.drop_handlers.append(
|
|
451
458
|
{"priority": priority, "plugin": plugin_name, "callback": callback}
|
|
452
459
|
)
|
|
@@ -456,6 +463,7 @@ class PluginManager:
|
|
|
456
463
|
def register_export_action(
|
|
457
464
|
self, plugin_name: str, label: str, callback: Callable
|
|
458
465
|
) -> None:
|
|
466
|
+
"""Register a plugin export action with its label and callback."""
|
|
459
467
|
self.export_actions.append(
|
|
460
468
|
{"plugin": plugin_name, "label": label, "callback": callback}
|
|
461
469
|
)
|
|
@@ -463,6 +471,7 @@ class PluginManager:
|
|
|
463
471
|
def register_optimization_method(
|
|
464
472
|
self, plugin_name: str, method_name: str, callback: Callable
|
|
465
473
|
) -> None:
|
|
474
|
+
"""Register a named 3D optimization method provided by a plugin."""
|
|
466
475
|
# Key by upper-case method name for consistency
|
|
467
476
|
self.optimization_methods[method_name.upper()] = {
|
|
468
477
|
"plugin": plugin_name,
|
|
@@ -473,6 +482,7 @@ class PluginManager:
|
|
|
473
482
|
def register_file_opener(
|
|
474
483
|
self, plugin_name: str, extension: str, callback: Callable, priority: int = 0
|
|
475
484
|
) -> None:
|
|
485
|
+
"""Register a file-type handler for the given extension."""
|
|
476
486
|
# Normalize extension to lowercase
|
|
477
487
|
ext = extension.lower()
|
|
478
488
|
if not ext.startswith("."):
|
|
@@ -492,20 +502,24 @@ class PluginManager:
|
|
|
492
502
|
def register_analysis_tool(
|
|
493
503
|
self, plugin_name: str, label: str, callback: Callable
|
|
494
504
|
) -> None:
|
|
505
|
+
"""Register a plugin analysis tool with its label and callback."""
|
|
495
506
|
self.analysis_tools.append(
|
|
496
507
|
{"plugin": plugin_name, "label": label, "callback": callback}
|
|
497
508
|
)
|
|
498
509
|
|
|
499
510
|
# State Persistence registration
|
|
500
511
|
def register_save_handler(self, plugin_name: str, callback: Callable) -> None:
|
|
512
|
+
"""Register a plugin session-save callback."""
|
|
501
513
|
self.save_handlers[plugin_name] = callback
|
|
502
514
|
|
|
503
515
|
def register_load_handler(self, plugin_name: str, callback: Callable) -> None:
|
|
516
|
+
"""Register a plugin session-load callback."""
|
|
504
517
|
self.load_handlers[plugin_name] = callback
|
|
505
518
|
|
|
506
519
|
def register_3d_style(
|
|
507
520
|
self, plugin_name: str, style_name: str, callback: Callable
|
|
508
521
|
) -> None:
|
|
522
|
+
"""Register a named custom 3D rendering style."""
|
|
509
523
|
self.custom_3d_styles[style_name] = {
|
|
510
524
|
"plugin": plugin_name,
|
|
511
525
|
"callback": callback,
|
{moleditpy_linux-4.0.1 → moleditpy_linux-4.1.0}/src/moleditpy_linux/plugins/plugin_manager_window.py
RENAMED
|
@@ -33,6 +33,8 @@ from PyQt6.QtWidgets import (
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class PluginManagerWindow(QDialog):
|
|
36
|
+
"""Dialog for browsing, installing, and removing plugins."""
|
|
37
|
+
|
|
36
38
|
def __init__(self, plugin_manager: Any, parent: Optional[QWidget] = None) -> None:
|
|
37
39
|
super().__init__(parent)
|
|
38
40
|
self.btn_remove = None
|
|
@@ -46,6 +48,7 @@ class PluginManagerWindow(QDialog):
|
|
|
46
48
|
self.refresh_plugin_list()
|
|
47
49
|
|
|
48
50
|
def init_ui(self) -> None:
|
|
51
|
+
"""Build the plugin manager UI with table, buttons, and drag-and-drop support."""
|
|
49
52
|
layout = QVBoxLayout(self)
|
|
50
53
|
|
|
51
54
|
lbl_info = QLabel("Drag & Drop .py or .zip files here to install plugins.")
|
|
@@ -106,6 +109,7 @@ class PluginManagerWindow(QDialog):
|
|
|
106
109
|
layout.addLayout(btn_layout)
|
|
107
110
|
|
|
108
111
|
def refresh_plugin_list(self) -> None:
|
|
112
|
+
"""Repopulate the plugin table from the current plugin registry."""
|
|
109
113
|
self.table.setRowCount(0)
|
|
110
114
|
plugins = self.plugin_manager.plugins
|
|
111
115
|
|
|
@@ -146,11 +150,13 @@ class PluginManagerWindow(QDialog):
|
|
|
146
150
|
self.table.item(row, 0).setForeground(color)
|
|
147
151
|
|
|
148
152
|
def update_button_state(self) -> None:
|
|
153
|
+
"""Enable or disable the Remove button based on table selection."""
|
|
149
154
|
has_selection = self.table.currentRow() >= 0
|
|
150
155
|
if hasattr(self, "btn_remove"):
|
|
151
156
|
self.btn_remove.setEnabled(has_selection)
|
|
152
157
|
|
|
153
158
|
def on_reload(self, silent: bool = False) -> None:
|
|
159
|
+
"""Reload all plugins from disk and refresh the table."""
|
|
154
160
|
# Trigger reload in main manager
|
|
155
161
|
if self.plugin_manager.main_window:
|
|
156
162
|
self.plugin_manager.discover_plugins(self.plugin_manager.main_window)
|
|
@@ -164,6 +170,7 @@ class PluginManagerWindow(QDialog):
|
|
|
164
170
|
self.refresh_plugin_list()
|
|
165
171
|
|
|
166
172
|
def on_remove_plugin(self) -> None:
|
|
173
|
+
"""Delete the selected plugin file or folder and reload."""
|
|
167
174
|
row = self.table.currentRow()
|
|
168
175
|
if row < 0:
|
|
169
176
|
QMessageBox.warning(self, "Warning", "Please select a plugin to remove.")
|
|
@@ -206,7 +213,7 @@ class PluginManagerWindow(QDialog):
|
|
|
206
213
|
"Success",
|
|
207
214
|
f"Removed '{plugin.get('name', 'Unknown')}'.",
|
|
208
215
|
)
|
|
209
|
-
except
|
|
216
|
+
except OSError as e:
|
|
210
217
|
QMessageBox.critical(
|
|
211
218
|
self, "Error", f"Failed to delete plugin: {e}"
|
|
212
219
|
)
|
|
@@ -216,6 +223,7 @@ class PluginManagerWindow(QDialog):
|
|
|
216
223
|
)
|
|
217
224
|
|
|
218
225
|
def show_plugin_details(self, item: QTableWidgetItem) -> None:
|
|
226
|
+
"""Show a message box with full metadata for the double-clicked plugin."""
|
|
219
227
|
row = item.row()
|
|
220
228
|
if row < len(self.plugin_manager.plugins):
|
|
221
229
|
p = self.plugin_manager.plugins[row]
|
|
@@ -231,6 +239,7 @@ class PluginManagerWindow(QDialog):
|
|
|
231
239
|
|
|
232
240
|
# --- Drag & Drop Support ---
|
|
233
241
|
def dragEnterEvent(self, event: Optional[QDragEnterEvent]) -> None:
|
|
242
|
+
"""Accept drag events carrying file URLs."""
|
|
234
243
|
if event is None:
|
|
235
244
|
return
|
|
236
245
|
if event.mimeData().hasUrls():
|
|
@@ -239,6 +248,7 @@ class PluginManagerWindow(QDialog):
|
|
|
239
248
|
event.ignore()
|
|
240
249
|
|
|
241
250
|
def dropEvent(self, event: Optional[QDropEvent]) -> None:
|
|
251
|
+
"""Install dropped plugin .py files, folders, or zip archives."""
|
|
242
252
|
if event is None:
|
|
243
253
|
return
|
|
244
254
|
files_installed = []
|
|
@@ -30,6 +30,8 @@ from ..utils.constants import VERSION
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class AboutDialog(QDialog):
|
|
33
|
+
"""Dialog showing application version, icon, and project links."""
|
|
34
|
+
|
|
33
35
|
def __init__(self, main_window: Any, parent: Optional[QWidget] = None) -> None:
|
|
34
36
|
super().__init__(parent)
|
|
35
37
|
self.image_label = None
|
|
@@ -39,6 +41,7 @@ class AboutDialog(QDialog):
|
|
|
39
41
|
self.init_ui()
|
|
40
42
|
|
|
41
43
|
def init_ui(self) -> None:
|
|
44
|
+
"""Build the about dialog layout with icon, version text, and links."""
|
|
42
45
|
layout = QVBoxLayout(self)
|
|
43
46
|
|
|
44
47
|
# Create a clickable image label
|
|
@@ -33,6 +33,8 @@ from rdkit.Chem import inchi as rd_inchi
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class AnalysisWindow(QDialog):
|
|
36
|
+
"""Dialog displaying molecular formula, weight, and other computed properties."""
|
|
37
|
+
|
|
36
38
|
def __init__(
|
|
37
39
|
self,
|
|
38
40
|
mol: Chem.Mol,
|
|
@@ -47,6 +49,7 @@ class AnalysisWindow(QDialog):
|
|
|
47
49
|
self.init_ui()
|
|
48
50
|
|
|
49
51
|
def init_ui(self) -> None:
|
|
52
|
+
"""Build the analysis layout with computed molecular properties."""
|
|
50
53
|
main_layout = QVBoxLayout(self)
|
|
51
54
|
grid_layout = QGridLayout()
|
|
52
55
|
|
|
@@ -230,6 +233,7 @@ class AnalysisWindow(QDialog):
|
|
|
230
233
|
self.setLayout(main_layout)
|
|
231
234
|
|
|
232
235
|
def copy_to_clipboard(self, text: str) -> None:
|
|
236
|
+
"""Copy text to the system clipboard and show a status bar message."""
|
|
233
237
|
clipboard = QApplication.clipboard()
|
|
234
238
|
clipboard.setText(text)
|
|
235
239
|
if self.parent() and hasattr(self.parent(), "statusBar"):
|