MoleditPy-linux 4.3.1__tar.gz → 4.4.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.3.1 → moleditpy_linux-4.4.0}/PKG-INFO +1 -1
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/pyproject.toml +1 -1
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/app_state.py +33 -14
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/custom_interactor_style.py +109 -8
- moleditpy_linux-4.4.0/src/moleditpy_linux/ui/custom_qt_interactor.py +75 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +2 -3
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/io_logic.py +89 -15
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +6 -2
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +14 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/ui_manager.py +43 -1
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/utils/constants.py +9 -1
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/utils/default_settings.py +2 -0
- moleditpy_linux-4.3.1/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -105
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/LICENSE +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/README.md +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/setup.cfg +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/core/mol_geometry.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/core/molecular_data.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/main.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/about_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/analysis_window.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/atom_item.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/atom_picking.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/bond_item.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/compute_logic.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/export_logic.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/main_window.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/main_window_init.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/string_importers.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/utils/suppress_log.py +0 -0
- {moleditpy_linux-4.3.1 → moleditpy_linux-4.4.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.
|
|
3
|
+
Version: 4.4.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.
|
|
3
|
+
Version: 4.4.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
|
|
@@ -420,6 +420,19 @@ class StateManager:
|
|
|
420
420
|
# Skip property access if atom data is inconsistent
|
|
421
421
|
original_id = None
|
|
422
422
|
|
|
423
|
+
# Implicit/explicit H counts raise on an unsanitized mol
|
|
424
|
+
# (e.g. an XYZ import whose valences never passed
|
|
425
|
+
# sanitization). Default to 0 so the structure still saves
|
|
426
|
+
# and round-trips via the binary rather than being dropped.
|
|
427
|
+
try:
|
|
428
|
+
num_explicit_hs = atom.GetNumExplicitHs()
|
|
429
|
+
except (RuntimeError, ValueError):
|
|
430
|
+
num_explicit_hs = 0
|
|
431
|
+
try:
|
|
432
|
+
num_implicit_hs = atom.GetNumImplicitHs()
|
|
433
|
+
except (RuntimeError, ValueError):
|
|
434
|
+
num_implicit_hs = 0
|
|
435
|
+
|
|
423
436
|
atom_3d = {
|
|
424
437
|
"index": i,
|
|
425
438
|
"symbol": atom.GetSymbol(),
|
|
@@ -428,8 +441,8 @@ class StateManager:
|
|
|
428
441
|
"y": pos.y,
|
|
429
442
|
"z": pos.z,
|
|
430
443
|
"formal_charge": atom.GetFormalCharge(),
|
|
431
|
-
"num_explicit_hs":
|
|
432
|
-
"num_implicit_hs":
|
|
444
|
+
"num_explicit_hs": num_explicit_hs,
|
|
445
|
+
"num_implicit_hs": num_implicit_hs,
|
|
433
446
|
# include original editor atom id when available for round-trip
|
|
434
447
|
"original_id": original_id,
|
|
435
448
|
}
|
|
@@ -462,17 +475,21 @@ class StateManager:
|
|
|
462
475
|
"constraints_3d": json_safe_constraints,
|
|
463
476
|
}
|
|
464
477
|
|
|
465
|
-
# Molecular info
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
"
|
|
469
|
-
|
|
470
|
-
self.host.view_3d_manager.current_mol
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
478
|
+
# Molecular info — MolWt/CalcMolFormula can raise on an
|
|
479
|
+
# unsanitized mol; keep it optional so it never blocks the save.
|
|
480
|
+
try:
|
|
481
|
+
json_data["molecular_info"] = {
|
|
482
|
+
"num_atoms": self.host.view_3d_manager.current_mol.GetNumAtoms(),
|
|
483
|
+
"num_bonds": self.host.view_3d_manager.current_mol.GetNumBonds(),
|
|
484
|
+
"molecular_weight": Descriptors.MolWt(
|
|
485
|
+
self.host.view_3d_manager.current_mol
|
|
486
|
+
),
|
|
487
|
+
"formula": rdMolDescriptors.CalcMolFormula(
|
|
488
|
+
self.host.view_3d_manager.current_mol
|
|
489
|
+
),
|
|
490
|
+
}
|
|
491
|
+
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
492
|
+
logging.warning("Could not compute molecular info: %s", e)
|
|
476
493
|
|
|
477
494
|
# Identifiers (SMILES/InChI)
|
|
478
495
|
try:
|
|
@@ -637,7 +654,9 @@ class StateManager:
|
|
|
637
654
|
ValueError,
|
|
638
655
|
TypeError,
|
|
639
656
|
IndexError,
|
|
640
|
-
):
|
|
657
|
+
):
|
|
658
|
+
# [RDKIT GUARD] property set may
|
|
659
|
+
# fail on invalid ids; skip.
|
|
641
660
|
logging.debug(
|
|
642
661
|
"Suppressed non-critical error",
|
|
643
662
|
exc_info=True,
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/custom_interactor_style.py
RENAMED
|
@@ -25,6 +25,16 @@ from .atom_picking import pick_atom_index_from_screen
|
|
|
25
25
|
|
|
26
26
|
from rdkit import Geometry
|
|
27
27
|
|
|
28
|
+
# VTK's trackball state id for camera rotation (VTKIS_ROTATE); the enum is not
|
|
29
|
+
# exposed to Python, so the literal is used with this name for clarity.
|
|
30
|
+
_VTKIS_ROTATE = 1
|
|
31
|
+
|
|
32
|
+
# Reference 3D-canvas size (px) that camera rotation speed is normalized to.
|
|
33
|
+
# VTK's stock Rotate divides motion by the live render size, so rotation slows
|
|
34
|
+
# as the canvas grows; normalizing to a fixed reference makes the speed
|
|
35
|
+
# window-size independent and matches the feel at the default window size.
|
|
36
|
+
_ROTATION_REFERENCE_SIZE = 640.0
|
|
37
|
+
|
|
28
38
|
|
|
29
39
|
class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
30
40
|
"""VTK interactor style extending trackball-camera with 3D atom drag and measurement."""
|
|
@@ -81,6 +91,43 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
81
91
|
# Safe defensive fallback catching AttributeError, RuntimeError
|
|
82
92
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
83
93
|
|
|
94
|
+
def _rotate_size_independent(self) -> None:
|
|
95
|
+
"""Rotate the camera at a speed that does not depend on canvas size.
|
|
96
|
+
|
|
97
|
+
Mirrors vtkInteractorStyleTrackballCamera::Rotate but normalizes the
|
|
98
|
+
per-pixel azimuth/elevation to a fixed reference size instead of the
|
|
99
|
+
live render size, and scales by the user's rotation-sensitivity setting.
|
|
100
|
+
"""
|
|
101
|
+
renderer = self.GetCurrentRenderer()
|
|
102
|
+
interactor = self.GetInteractor()
|
|
103
|
+
if renderer is None or interactor is None:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
dx = interactor.GetEventPosition()[0] - interactor.GetLastEventPosition()[0]
|
|
107
|
+
dy = interactor.GetEventPosition()[1] - interactor.GetLastEventPosition()[1]
|
|
108
|
+
|
|
109
|
+
sensitivity = 1.0
|
|
110
|
+
mw = self.main_window
|
|
111
|
+
if mw is not None:
|
|
112
|
+
try:
|
|
113
|
+
sensitivity = float(
|
|
114
|
+
mw.get_settings().get("mouse_rotation_sensitivity", 1.0)
|
|
115
|
+
)
|
|
116
|
+
except (AttributeError, TypeError, ValueError):
|
|
117
|
+
sensitivity = 1.0
|
|
118
|
+
|
|
119
|
+
delta = -20.0 / _ROTATION_REFERENCE_SIZE * self.GetMotionFactor() * sensitivity
|
|
120
|
+
camera = renderer.GetActiveCamera()
|
|
121
|
+
camera.Azimuth(dx * delta)
|
|
122
|
+
camera.Elevation(dy * delta)
|
|
123
|
+
camera.OrthogonalizeViewUp()
|
|
124
|
+
|
|
125
|
+
if self.GetAutoAdjustCameraClippingRange():
|
|
126
|
+
renderer.ResetCameraClippingRange()
|
|
127
|
+
if interactor.GetLightFollowCamera():
|
|
128
|
+
renderer.UpdateLightsGeometryToFollowCamera()
|
|
129
|
+
interactor.Render()
|
|
130
|
+
|
|
84
131
|
def _stop_vtk_left_button_state(self) -> None:
|
|
85
132
|
"""Clear VTK's button/drag state after a custom-handled left click."""
|
|
86
133
|
try:
|
|
@@ -159,9 +206,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
159
206
|
try:
|
|
160
207
|
move_group_dialog.on_atom_picked(clicked_atom_idx)
|
|
161
208
|
except (AttributeError, RuntimeError):
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"Suppressed non-critical error", exc_info=True
|
|
209
|
+
logging.warning(
|
|
210
|
+
"Move-dialog atom toggle failed", exc_info=True
|
|
165
211
|
)
|
|
166
212
|
|
|
167
213
|
QTimer.singleShot(0, _deferred_toggle)
|
|
@@ -273,9 +319,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
273
319
|
closest_atom_idx
|
|
274
320
|
)
|
|
275
321
|
except (AttributeError, RuntimeError):
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
"Suppressed non-critical error", exc_info=True
|
|
322
|
+
logging.warning(
|
|
323
|
+
"Measurement selection failed", exc_info=True
|
|
279
324
|
)
|
|
280
325
|
|
|
281
326
|
QTimer.singleShot(0, _deferred_measure)
|
|
@@ -383,6 +428,54 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
383
428
|
# Standard right-click
|
|
384
429
|
super().OnRightButtonDown()
|
|
385
430
|
|
|
431
|
+
def _heal_stuck_pointer_state(self, move_group_dialog: Any) -> None:
|
|
432
|
+
"""Self-heal drag/rotate state whose release event was lost.
|
|
433
|
+
|
|
434
|
+
A release can be lost mid-gesture (e.g. pyvista temporarily swapping
|
|
435
|
+
the interactor style, a dialog grabbing events), leaving this style or
|
|
436
|
+
a Move Group dialog in a permanent drag/rotate state that blocks
|
|
437
|
+
interaction. Verify against the real button state on every move and
|
|
438
|
+
reset when the button is not actually held.
|
|
439
|
+
"""
|
|
440
|
+
try:
|
|
441
|
+
buttons = QApplication.mouseButtons()
|
|
442
|
+
left_held = bool(buttons & Qt.MouseButton.LeftButton)
|
|
443
|
+
right_held = bool(buttons & Qt.MouseButton.RightButton)
|
|
444
|
+
any_held = buttons != Qt.MouseButton.NoButton
|
|
445
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
if self._is_dragging_atom and not left_held:
|
|
449
|
+
self.reset_interactor_state()
|
|
450
|
+
|
|
451
|
+
if not any_held:
|
|
452
|
+
try:
|
|
453
|
+
if self.GetState() != 0: # stuck ROTATE/PAN/etc. without a button
|
|
454
|
+
self.reset_interactor_state()
|
|
455
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
456
|
+
# Safe defensive fallback catching AttributeError, RuntimeError, TypeError
|
|
457
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
458
|
+
|
|
459
|
+
if move_group_dialog is not None:
|
|
460
|
+
try:
|
|
461
|
+
if (
|
|
462
|
+
getattr(move_group_dialog, "is_dragging_group_vtk", False)
|
|
463
|
+
and not left_held
|
|
464
|
+
):
|
|
465
|
+
move_group_dialog.is_dragging_group_vtk = False
|
|
466
|
+
move_group_dialog.drag_start_pos_vtk = None
|
|
467
|
+
move_group_dialog.mouse_moved_vtk = False
|
|
468
|
+
if (
|
|
469
|
+
getattr(move_group_dialog, "is_rotating_group_vtk", False)
|
|
470
|
+
and not right_held
|
|
471
|
+
):
|
|
472
|
+
move_group_dialog.is_rotating_group_vtk = False
|
|
473
|
+
move_group_dialog.rotation_start_pos = None
|
|
474
|
+
move_group_dialog.rotation_mouse_moved = False
|
|
475
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
476
|
+
# Safe defensive fallback catching AttributeError, RuntimeError, TypeError
|
|
477
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
478
|
+
|
|
386
479
|
def on_mouse_move(self, obj: Any, event: Any) -> None:
|
|
387
480
|
"""
|
|
388
481
|
Handle mouse move (drag vs camera/hover).
|
|
@@ -402,6 +495,8 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
402
495
|
break
|
|
403
496
|
except (AttributeError, RuntimeError, TypeError):
|
|
404
497
|
logging.warning("Caught exception in " + __file__, exc_info=True)
|
|
498
|
+
|
|
499
|
+
self._heal_stuck_pointer_state(move_group_dialog)
|
|
405
500
|
if move_group_dialog and getattr(
|
|
406
501
|
move_group_dialog, "is_dragging_group_vtk", False
|
|
407
502
|
):
|
|
@@ -453,8 +548,14 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
|
|
|
453
548
|
# Custom atom drag
|
|
454
549
|
self.is_dragging = True
|
|
455
550
|
else:
|
|
456
|
-
#
|
|
457
|
-
|
|
551
|
+
# Camera interaction. VTK's built-in Rotate divides mouse motion by
|
|
552
|
+
# the live render size, so rotation slows as the canvas grows. Handle
|
|
553
|
+
# the rotate state ourselves for window-size-independent speed;
|
|
554
|
+
# delegate pan/dolly/spin to the parent unchanged.
|
|
555
|
+
if self.GetState() == _VTKIS_ROTATE:
|
|
556
|
+
self._rotate_size_independent()
|
|
557
|
+
else:
|
|
558
|
+
super().OnMouseMove()
|
|
458
559
|
|
|
459
560
|
# Update cursor display
|
|
460
561
|
is_edit_active = (
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
MoleditPy — A Python-based molecular editing software
|
|
6
|
+
|
|
7
|
+
Author: Hiromichi Yokoyama
|
|
8
|
+
License: GPL-3.0 license
|
|
9
|
+
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
+
DOI: 10.5281/zenodo.17268532
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
from PyQt6.QtCore import QEvent
|
|
17
|
+
from PyQt6.QtGui import QMouseEvent
|
|
18
|
+
from pyvistaqt import QtInteractor
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CustomQtInteractor(QtInteractor):
|
|
22
|
+
"""PyVista QtInteractor subclass that exposes the main window for event handling."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
parent: Optional[Any] = None,
|
|
27
|
+
main_window: Optional[Any] = None,
|
|
28
|
+
**kwargs: Any,
|
|
29
|
+
) -> None:
|
|
30
|
+
super().__init__(parent, **kwargs)
|
|
31
|
+
self.main_window = main_window
|
|
32
|
+
|
|
33
|
+
def wheelEvent(self, event: Any) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Override the mouse wheel event.
|
|
36
|
+
"""
|
|
37
|
+
# First call the parent class event to perform normal zoom processing
|
|
38
|
+
super().wheelEvent(event)
|
|
39
|
+
|
|
40
|
+
# Force focus back to the 2D view after zoom processing
|
|
41
|
+
if self.main_window and hasattr(self.main_window.init_manager, "view_2d"):
|
|
42
|
+
self.main_window.init_manager.view_2d.setFocus()
|
|
43
|
+
|
|
44
|
+
def mouseReleaseEvent(self, event: Any) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Override the Qt mouse release event to return focus to the 2D view after
|
|
47
|
+
all 3D view operations.
|
|
48
|
+
"""
|
|
49
|
+
super().mouseReleaseEvent(event) # Process parent class event first
|
|
50
|
+
if self.main_window and hasattr(self.main_window.init_manager, "view_2d"):
|
|
51
|
+
self.main_window.init_manager.view_2d.setFocus()
|
|
52
|
+
|
|
53
|
+
def mouseDoubleClickEvent(self, event: Any) -> None:
|
|
54
|
+
"""Re-dispatch double-clicks as plain presses so fast clicking works.
|
|
55
|
+
|
|
56
|
+
Qt turns every second fast click into a double-click event. Forwarding
|
|
57
|
+
it unchanged would reach VTK with a repeat count and be dropped by the
|
|
58
|
+
interactor style, so the click would be lost (e.g. when rapidly
|
|
59
|
+
selecting atoms in measurement mode). Synthesizing a normal press keeps
|
|
60
|
+
press/release pairing intact and makes each fast click act as a click.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
synthetic_press = QMouseEvent(
|
|
64
|
+
QEvent.Type.MouseButtonPress,
|
|
65
|
+
event.position(),
|
|
66
|
+
event.globalPosition(),
|
|
67
|
+
event.button(),
|
|
68
|
+
event.buttons(),
|
|
69
|
+
event.modifiers(),
|
|
70
|
+
)
|
|
71
|
+
super().mousePressEvent(synthetic_press)
|
|
72
|
+
event.accept()
|
|
73
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
74
|
+
# Safe defensive fallback catching AttributeError, RuntimeError, TypeError
|
|
75
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py
RENAMED
|
@@ -82,9 +82,8 @@ class Dialog3DPickingMixin:
|
|
|
82
82
|
try:
|
|
83
83
|
self.on_atom_picked(int(closest_atom_idx))
|
|
84
84
|
except (AttributeError, RuntimeError):
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"Suppressed non-critical error", exc_info=True
|
|
85
|
+
logging.warning(
|
|
86
|
+
"Dialog atom pick failed", exc_info=True
|
|
88
87
|
)
|
|
89
88
|
|
|
90
89
|
from PyQt6.QtCore import QTimer
|
|
@@ -35,7 +35,12 @@ from PyQt6.QtWidgets import (
|
|
|
35
35
|
from rdkit import Chem
|
|
36
36
|
from rdkit.Chem import AllChem, rdGeometry, rdMolTransforms, Descriptors
|
|
37
37
|
|
|
38
|
-
from ..utils.constants import
|
|
38
|
+
from ..utils.constants import (
|
|
39
|
+
COVALENT_RADII,
|
|
40
|
+
DUMMY_XYZ_SYMBOLS,
|
|
41
|
+
VALID_ELEMENT_SYMBOLS,
|
|
42
|
+
VERSION,
|
|
43
|
+
)
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
class IOManager:
|
|
@@ -97,6 +102,38 @@ class IOManager:
|
|
|
97
102
|
lines[3] = self.fix_mol_counts_line(lines[3])
|
|
98
103
|
return "\n".join(lines)
|
|
99
104
|
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _is_numeric_token(token: str) -> bool:
|
|
107
|
+
"""True if *token* parses as a float."""
|
|
108
|
+
try:
|
|
109
|
+
float(token)
|
|
110
|
+
return True
|
|
111
|
+
except ValueError:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _extract_xyz_coords(tokens: list[str]) -> Optional[Tuple[float, float, float]]:
|
|
116
|
+
"""Return the first three float-parseable tokens as (x, y, z).
|
|
117
|
+
|
|
118
|
+
Skips non-numeric separator/label columns so ghost-atom rows like
|
|
119
|
+
``XX : x y z`` (a colon in the second column) parse the same as a
|
|
120
|
+
standard ``symbol x y z`` row. Returns None if fewer than three numbers
|
|
121
|
+
are present.
|
|
122
|
+
"""
|
|
123
|
+
nums: list[float] = []
|
|
124
|
+
for tok in tokens:
|
|
125
|
+
try:
|
|
126
|
+
nums.append(float(tok))
|
|
127
|
+
except ValueError:
|
|
128
|
+
if nums:
|
|
129
|
+
break # trailing non-numeric column after the coordinates
|
|
130
|
+
continue # leading label/separator column before the coordinates
|
|
131
|
+
if len(nums) == 3:
|
|
132
|
+
break
|
|
133
|
+
if len(nums) == 3:
|
|
134
|
+
return (nums[0], nums[1], nums[2])
|
|
135
|
+
return None
|
|
136
|
+
|
|
100
137
|
def _normalize_xyz_symbol(self, raw_symbol: str) -> Tuple[str, bool]:
|
|
101
138
|
"""Return the RDKit symbol for an XYZ atom and whether it is a dummy."""
|
|
102
139
|
stripped = raw_symbol.strip()
|
|
@@ -105,11 +142,11 @@ class IOManager:
|
|
|
105
142
|
if stripped.upper() in DUMMY_XYZ_SYMBOLS:
|
|
106
143
|
return "*", True
|
|
107
144
|
symbol = stripped.capitalize()
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if
|
|
145
|
+
# Membership test rather than GetAtomicNumber(symbol): the latter makes
|
|
146
|
+
# RDKit's C++ layer print an "Element 'Xx' not found" violation to
|
|
147
|
+
# stderr for any dummy/pseudo label even though we catch the exception,
|
|
148
|
+
# which users read as a failed load.
|
|
149
|
+
if symbol not in VALID_ELEMENT_SYMBOLS:
|
|
113
150
|
return "*", True
|
|
114
151
|
return symbol, False
|
|
115
152
|
|
|
@@ -138,9 +175,19 @@ class IOManager:
|
|
|
138
175
|
|
|
139
176
|
atoms_data = []
|
|
140
177
|
has_dummy_atoms = False
|
|
141
|
-
|
|
178
|
+
nonstandard_rows = 0
|
|
179
|
+
atom_end = atom_start + num_atoms
|
|
180
|
+
atom_lines = lines[atom_start:atom_end]
|
|
142
181
|
if len(atom_lines) < num_atoms:
|
|
143
|
-
|
|
182
|
+
# Header count exceeds the rows present (truncated / miscounted file).
|
|
183
|
+
# Load the atoms that are actually there rather than refusing the
|
|
184
|
+
# whole file, so any parseable XYZ still opens.
|
|
185
|
+
logging.warning(
|
|
186
|
+
"XYZ declares %d atoms but only %d rows present; loading %d.",
|
|
187
|
+
num_atoms,
|
|
188
|
+
len(atom_lines),
|
|
189
|
+
len(atom_lines),
|
|
190
|
+
)
|
|
144
191
|
|
|
145
192
|
for i, line in enumerate(atom_lines):
|
|
146
193
|
parts = line.split()
|
|
@@ -149,9 +196,15 @@ class IOManager:
|
|
|
149
196
|
raw_symbol = parts[0]
|
|
150
197
|
symbol, is_dummy = self._normalize_xyz_symbol(raw_symbol)
|
|
151
198
|
has_dummy_atoms = has_dummy_atoms or is_dummy
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
199
|
+
coords = self._extract_xyz_coords(parts[1:])
|
|
200
|
+
if coords is None:
|
|
201
|
+
raise ValueError(f"Invalid atom data at line {atom_start + i + 1}")
|
|
202
|
+
# Standard rows put coordinates in the first three columns after the
|
|
203
|
+
# symbol; a non-numeric first column means an extra separator/label
|
|
204
|
+
# (e.g. the ':' in "XX : x y z") was skipped — flag it for the user.
|
|
205
|
+
if not self._is_numeric_token(parts[1]):
|
|
206
|
+
nonstandard_rows += 1
|
|
207
|
+
atoms_data.append((symbol, *coords))
|
|
155
208
|
|
|
156
209
|
if not atoms_data:
|
|
157
210
|
raise ValueError("No valid atoms found in XYZ file")
|
|
@@ -162,6 +215,8 @@ class IOManager:
|
|
|
162
215
|
atom = Chem.Atom(symbol)
|
|
163
216
|
atom.SetIntProp("xyz_unique_id", i)
|
|
164
217
|
if atom.GetAtomicNum() == 0:
|
|
218
|
+
# Ghost/dummy rows (e.g. "XX : x y z") load as the wildcard "*";
|
|
219
|
+
# stash the real label token so it is kept for round-tripping.
|
|
165
220
|
atom.SetProp("xyz_original_symbol", atom_lines[i].split()[0])
|
|
166
221
|
mol.AddAtom(atom)
|
|
167
222
|
conf.SetAtomPosition(i, rdGeometry.Point3D(x, y, z))
|
|
@@ -190,7 +245,16 @@ class IOManager:
|
|
|
190
245
|
_set_prop(candidate, "_xyz_charge", charge_val)
|
|
191
246
|
return candidate
|
|
192
247
|
else:
|
|
193
|
-
|
|
248
|
+
# Distance-based bonding is best-effort: a failure here must not
|
|
249
|
+
# lose the molecule. The skip/dummy path only needs atoms and
|
|
250
|
+
# coordinates, so fall back to a bond-free structure so that any
|
|
251
|
+
# parseable XYZ still loads.
|
|
252
|
+
try:
|
|
253
|
+
self.estimate_bonds_from_distances(mol)
|
|
254
|
+
except (RuntimeError, ValueError, TypeError, AttributeError) as e:
|
|
255
|
+
logging.warning(
|
|
256
|
+
"Bond estimation failed; loading atoms without bonds: %s", e
|
|
257
|
+
)
|
|
194
258
|
candidate = mol.GetMol()
|
|
195
259
|
_set_prop(candidate, "_xyz_charge", charge_val)
|
|
196
260
|
return candidate
|
|
@@ -240,6 +304,8 @@ class IOManager:
|
|
|
240
304
|
|
|
241
305
|
if final_mol:
|
|
242
306
|
final_mol.xyz_atom_data = atoms_data
|
|
307
|
+
if nonstandard_rows:
|
|
308
|
+
_set_prop(final_mol, "_xyz_nonstandard_rows", nonstandard_rows)
|
|
243
309
|
return final_mol
|
|
244
310
|
|
|
245
311
|
def _report_load_error(self, title: str, message: str) -> None:
|
|
@@ -866,9 +932,17 @@ class IOManager:
|
|
|
866
932
|
self.host.view_3d_manager.update_atom_id_menu_state()
|
|
867
933
|
|
|
868
934
|
if self.host.statusBar():
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
)
|
|
935
|
+
message = f"3D Viewer Mode: Loaded {os.path.basename(file_path)}"
|
|
936
|
+
nonstandard = 0
|
|
937
|
+
with suppress_log(AttributeError, KeyError, RuntimeError, TypeError):
|
|
938
|
+
if mol.HasProp("_xyz_nonstandard_rows"):
|
|
939
|
+
nonstandard = mol.GetIntProp("_xyz_nonstandard_rows")
|
|
940
|
+
if nonstandard:
|
|
941
|
+
message += (
|
|
942
|
+
f" (non-standard columns in {nonstandard} row(s); "
|
|
943
|
+
"coordinates read from the numeric columns)"
|
|
944
|
+
)
|
|
945
|
+
self.host.statusBar().showMessage(message)
|
|
872
946
|
self.host.set_current_file_path(file_path)
|
|
873
947
|
self.host.set_has_unsaved_changes(False)
|
|
874
948
|
self.host.state_manager.update_window_title()
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py
RENAMED
|
@@ -696,9 +696,13 @@ class MoveSelectedAtomsDialog(BasePickingDialog):
|
|
|
696
696
|
else:
|
|
697
697
|
btn.setText("Box Selection: OFF")
|
|
698
698
|
plotter.disable_picking()
|
|
699
|
-
# Restore original style
|
|
699
|
+
# Restore original style via pyvista's bookkeeping so its
|
|
700
|
+
# update_style() re-asserts ours instead of RubberBandPick
|
|
700
701
|
if self.original_style is not None:
|
|
701
|
-
|
|
702
|
+
try:
|
|
703
|
+
plotter.iren.style = self.original_style
|
|
704
|
+
except AttributeError:
|
|
705
|
+
plotter.interactor.SetInteractorStyle(self.original_style)
|
|
702
706
|
|
|
703
707
|
def on_rectangle_picked(self, selection: Any) -> None:
|
|
704
708
|
"""Handle PyVista rectangle picking callback."""
|
|
@@ -78,6 +78,16 @@ class Settings3DSceneTab(SettingsTabBase):
|
|
|
78
78
|
self.projection_combo.addItems(["Perspective", "Orthographic"])
|
|
79
79
|
form_layout.addRow("Projection Mode:", self.projection_combo)
|
|
80
80
|
|
|
81
|
+
# Camera rotation speed (0.10x–3.00x). Speed is normalized to a fixed
|
|
82
|
+
# reference canvas size, so it stays constant regardless of window size.
|
|
83
|
+
self.rotation_sens_slider, self.rotation_sens_label = self._create_slider(
|
|
84
|
+
10, 300, 100.0
|
|
85
|
+
)
|
|
86
|
+
form_layout.addRow(
|
|
87
|
+
"Mouse Rotation Speed:",
|
|
88
|
+
self._wrap_layout(self.rotation_sens_slider, self.rotation_sens_label),
|
|
89
|
+
)
|
|
90
|
+
|
|
81
91
|
def _select_color(self) -> None:
|
|
82
92
|
color = QColorDialog.getColor(QColor(self.current_bg_color), self)
|
|
83
93
|
if color.isValid():
|
|
@@ -111,6 +121,9 @@ class Settings3DSceneTab(SettingsTabBase):
|
|
|
111
121
|
if idx >= 0:
|
|
112
122
|
self.projection_combo.setCurrentIndex(idx)
|
|
113
123
|
|
|
124
|
+
sens_val = settings_dict.get("mouse_rotation_sensitivity", 1.0)
|
|
125
|
+
self.rotation_sens_slider.setValue(int(sens_val * 100))
|
|
126
|
+
|
|
114
127
|
def get_settings(self) -> dict[str, Any]:
|
|
115
128
|
return {
|
|
116
129
|
"background_color": self.current_bg_color,
|
|
@@ -120,6 +133,7 @@ class Settings3DSceneTab(SettingsTabBase):
|
|
|
120
133
|
"specular": self.specular_slider.value() / 100.0,
|
|
121
134
|
"specular_power": self.spec_power_slider.value(),
|
|
122
135
|
"projection_mode": self.projection_combo.currentText(),
|
|
136
|
+
"mouse_rotation_sensitivity": self.rotation_sens_slider.value() / 100.0,
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
|
|
@@ -322,9 +322,51 @@ class UIManager(QObject):
|
|
|
322
322
|
style = CustomInteractorStyle(self.host)
|
|
323
323
|
|
|
324
324
|
# Set interactor style
|
|
325
|
-
self.
|
|
325
|
+
self._install_interactor_style(style)
|
|
326
326
|
self.host.view_3d_manager.plotter.interactor.Initialize()
|
|
327
327
|
|
|
328
|
+
# Watchdog: if anything silently replaces the custom style (observed as
|
|
329
|
+
# "rotation works but pick/select is dead"), report it and reinstall.
|
|
330
|
+
self._expected_style = style
|
|
331
|
+
self._style_watchdog = QTimer(self)
|
|
332
|
+
self._style_watchdog.setInterval(2000)
|
|
333
|
+
self._style_watchdog.timeout.connect(self._check_interactor_style)
|
|
334
|
+
self._style_watchdog.start()
|
|
335
|
+
|
|
336
|
+
def _install_interactor_style(self, style: Any) -> None:
|
|
337
|
+
"""Install *style* through pyvista's bookkeeping, not raw VTK.
|
|
338
|
+
|
|
339
|
+
pyvista's RenderWindowInteractor re-asserts its own ``_style_class``
|
|
340
|
+
via ``update_style()`` whenever its built-in double-click chart
|
|
341
|
+
handler fires (fast clicks!). Installing through the ``iren.style``
|
|
342
|
+
property keeps that bookkeeping pointing at our style so the
|
|
343
|
+
re-assert is a no-op instead of an eviction.
|
|
344
|
+
"""
|
|
345
|
+
plotter = self.host.view_3d_manager.plotter
|
|
346
|
+
try:
|
|
347
|
+
plotter.iren.style = style
|
|
348
|
+
except AttributeError:
|
|
349
|
+
plotter.interactor.SetInteractorStyle(style)
|
|
350
|
+
|
|
351
|
+
def _check_interactor_style(self) -> None:
|
|
352
|
+
"""Reinstall CustomInteractorStyle if it was silently replaced."""
|
|
353
|
+
try:
|
|
354
|
+
interactor = self.host.view_3d_manager.plotter.interactor
|
|
355
|
+
current = interactor.GetInteractorStyle()
|
|
356
|
+
if current is self._expected_style:
|
|
357
|
+
return
|
|
358
|
+
name = type(current).__name__
|
|
359
|
+
if "RubberBand" in name: # legitimate temporary box-selection style
|
|
360
|
+
return
|
|
361
|
+
logging.debug(
|
|
362
|
+
"3D interactor style was replaced by %s — reinstalling "
|
|
363
|
+
"CustomInteractorStyle",
|
|
364
|
+
name,
|
|
365
|
+
)
|
|
366
|
+
self._install_interactor_style(self._expected_style)
|
|
367
|
+
except (AttributeError, RuntimeError):
|
|
368
|
+
logging.debug("Style watchdog check failed", exc_info=True)
|
|
369
|
+
|
|
328
370
|
def handle_drag_enter_event(self, event: QDragEnterEvent) -> None:
|
|
329
371
|
"""Internal handler for drag enter event (bypasses PyQt type checks in tests)."""
|
|
330
372
|
mime_data = event.mimeData()
|
|
@@ -24,7 +24,8 @@ def _get_version() -> str:
|
|
|
24
24
|
|
|
25
25
|
try:
|
|
26
26
|
return version("MoleditPy-linux")
|
|
27
|
-
except PackageNotFoundError:
|
|
27
|
+
except PackageNotFoundError:
|
|
28
|
+
# [OPTIONAL] Not installed in editable mode; fall through to pyproject.toml.
|
|
28
29
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
29
30
|
except ImportError:
|
|
30
31
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
@@ -209,6 +210,13 @@ DEFAULT_CPK_COLORS = {
|
|
|
209
210
|
|
|
210
211
|
|
|
211
212
|
pt = Chem.GetPeriodicTable()
|
|
213
|
+
# Real element symbols (H..Og). Membership-test against this instead of calling
|
|
214
|
+
# GetAtomicNumber() on an unknown label: probing the periodic table with a
|
|
215
|
+
# non-element makes RDKit's C++ layer print an alarming "Element 'Xx' not found"
|
|
216
|
+
# violation to stderr even when the Python exception is caught.
|
|
217
|
+
VALID_ELEMENT_SYMBOLS: frozenset[str] = frozenset(
|
|
218
|
+
pt.GetElementSymbol(i) for i in range(1, 119)
|
|
219
|
+
)
|
|
212
220
|
# 0.3-scaled vdW radii for 3D display only — not physical values
|
|
213
221
|
VDW_DISPLAY_RADII = {pt.GetElementSymbol(i): pt.GetRvdw(i) * 0.3 for i in range(1, 119)}
|
|
214
222
|
# Deprecated alias kept for external plugins
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/utils/default_settings.py
RENAMED
|
@@ -19,6 +19,8 @@ DEFAULT_SETTINGS = {
|
|
|
19
19
|
"specular_power": 20,
|
|
20
20
|
"light_intensity": 1.0,
|
|
21
21
|
"show_3d_axes": True,
|
|
22
|
+
# Camera rotation speed multiplier for the 3D scene (window-size independent).
|
|
23
|
+
"mouse_rotation_sensitivity": 1.0,
|
|
22
24
|
# --- 3D Model Parameters (Ball and Stick) ---
|
|
23
25
|
"ball_stick_atom_scale": 1.0,
|
|
24
26
|
"ball_stick_bond_radius": 0.1,
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
MoleditPy — A Python-based molecular editing software
|
|
6
|
-
|
|
7
|
-
Author: Hiromichi Yokoyama
|
|
8
|
-
License: GPL-3.0 license
|
|
9
|
-
Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
10
|
-
DOI: 10.5281/zenodo.17268532
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import time
|
|
14
|
-
from typing import Any, Optional
|
|
15
|
-
|
|
16
|
-
from pyvistaqt import QtInteractor
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class CustomQtInteractor(QtInteractor):
|
|
20
|
-
"""PyVista QtInteractor subclass that exposes the main window for event handling."""
|
|
21
|
-
|
|
22
|
-
def __init__(
|
|
23
|
-
self,
|
|
24
|
-
parent: Optional[Any] = None,
|
|
25
|
-
main_window: Optional[Any] = None,
|
|
26
|
-
**kwargs: Any,
|
|
27
|
-
) -> None:
|
|
28
|
-
super().__init__(parent, **kwargs)
|
|
29
|
-
self.main_window = main_window
|
|
30
|
-
self._last_click_time = 0.0
|
|
31
|
-
self._click_count = 0
|
|
32
|
-
self._ignore_next_release = False
|
|
33
|
-
|
|
34
|
-
def wheelEvent(self, event: Any) -> None:
|
|
35
|
-
"""
|
|
36
|
-
Override the mouse wheel event.
|
|
37
|
-
"""
|
|
38
|
-
# First call the parent class event to perform normal zoom processing
|
|
39
|
-
super().wheelEvent(event)
|
|
40
|
-
|
|
41
|
-
# Force focus back to the 2D view after zoom processing
|
|
42
|
-
if self.main_window and hasattr(self.main_window.init_manager, "view_2d"):
|
|
43
|
-
self.main_window.init_manager.view_2d.setFocus()
|
|
44
|
-
|
|
45
|
-
def mouseReleaseEvent(self, event: Any) -> None:
|
|
46
|
-
"""
|
|
47
|
-
Override the Qt mouse release event to return focus to the 2D view after
|
|
48
|
-
all 3D view operations. Also filters out "Ghost Release" (release without
|
|
49
|
-
a corresponding press).
|
|
50
|
-
"""
|
|
51
|
-
if self._ignore_next_release:
|
|
52
|
-
self._ignore_next_release = False
|
|
53
|
-
event.accept()
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
super().mouseReleaseEvent(event) # Process parent class event first
|
|
57
|
-
if self.main_window and hasattr(self.main_window.init_manager, "view_2d"):
|
|
58
|
-
self.main_window.init_manager.view_2d.setFocus()
|
|
59
|
-
|
|
60
|
-
def mousePressEvent(self, event: Any) -> None:
|
|
61
|
-
"""
|
|
62
|
-
Custom mouse press handling to track accumulated clicks and filter out
|
|
63
|
-
triple-clicks.
|
|
64
|
-
"""
|
|
65
|
-
current_time = time.time()
|
|
66
|
-
# Reset count if too much time has passed (0.5s is standard double-click time)
|
|
67
|
-
if current_time - self._last_click_time > 0.5:
|
|
68
|
-
self._click_count = 0
|
|
69
|
-
|
|
70
|
-
self._click_count += 1
|
|
71
|
-
self._last_click_time = current_time
|
|
72
|
-
|
|
73
|
-
# If this is the 3rd click (or more), swallow it to prevent
|
|
74
|
-
# the internal state desync that happens with rapid clicking sequences.
|
|
75
|
-
if self._click_count >= 3:
|
|
76
|
-
self._ignore_next_release = True
|
|
77
|
-
event.accept()
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
super().mousePressEvent(event)
|
|
81
|
-
|
|
82
|
-
def mouseDoubleClickEvent(self, event: Any) -> None:
|
|
83
|
-
"""Ignore mouse double-clicks on the 3D widget to avoid accidental actions.
|
|
84
|
-
|
|
85
|
-
Swallow the double-click event so it doesn't trigger selection, editing,
|
|
86
|
-
or camera jumps. We intentionally do not call the superclass handler.
|
|
87
|
-
Crucially, we also flag the NEXT release event to be swallowed, preventing
|
|
88
|
-
a "Ghost Release" (Release without Press) from reaching VTK.
|
|
89
|
-
"""
|
|
90
|
-
current_time = time.time()
|
|
91
|
-
self._last_click_time = current_time
|
|
92
|
-
# Set to 2 to ensure the next click counts as 3rd
|
|
93
|
-
if current_time - self._last_click_time < 0.5:
|
|
94
|
-
self._click_count = 2
|
|
95
|
-
else:
|
|
96
|
-
self._click_count = 2 # Force sync
|
|
97
|
-
|
|
98
|
-
self._ignore_next_release = True
|
|
99
|
-
|
|
100
|
-
try:
|
|
101
|
-
# Accept the event to mark it handled and prevent further processing.
|
|
102
|
-
event.accept()
|
|
103
|
-
except (AttributeError, RuntimeError, TypeError):
|
|
104
|
-
# If event doesn't support accept for some reason, just return.
|
|
105
|
-
return
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/MoleditPy_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/MoleditPy_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/plugins/plugin_manager_window.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/align_plane_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/base_picking_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/bond_length_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/calculation_worker.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/color_settings_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/edit_actions_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/geometry_base_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/molecular_scene_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/periodic_table_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/plugin_menu_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/settings_tabs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/template_preview_item.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/template_preview_view.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/translation_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/ui/user_template_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.3.1 → moleditpy_linux-4.4.0}/src/moleditpy_linux/utils/sip_isdeleted_safe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|