MoleditPy-linux 4.5.2__tar.gz → 4.6.1__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.5.2 → moleditpy_linux-4.6.1}/PKG-INFO +2 -1
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/README.md +1 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/pyproject.toml +1 -1
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/MoleditPy_linux.egg-info/PKG-INFO +2 -1
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/MoleditPy_linux.egg-info/SOURCES.txt +1 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/atom_item.py +16 -3
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/bond_item.py +18 -4
- moleditpy_linux-4.6.1/src/moleditpy_linux/ui/chain_mixin.py +178 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/main_window_init.py +64 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/molecular_scene_handler.py +18 -2
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/molecule_scene.py +33 -3
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +4 -1
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/template_preview_item.py +62 -1
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/ui_manager.py +13 -1
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/zoomable_view.py +1 -1
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/LICENSE +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/setup.cfg +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/core/mol_geometry.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/core/molecular_data.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/main.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/about_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/analysis_window.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/app_state.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/atom_picking.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/compute_logic.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/export_logic.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/io_logic.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/main_window.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/string_importers.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/utils/constants.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/utils/default_settings.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/utils/suppress_log.py +0 -0
- {moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/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.6.1
|
|
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
|
|
@@ -742,6 +742,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
|
|
|
742
742
|
|
|
743
743
|
* **Intuitive Operations:** Easily add, edit, and delete atoms and bonds with simple mouse controls. Left-click and drag to create, and right-click to delete.
|
|
744
744
|
* **Advanced Templates:** Place templates for benzene or 3- to 9-membered rings with a live preview. Features intelligent logic to automatically adjust the double-bond configuration when fusing a benzene ring to an existing structure.
|
|
745
|
+
* **Alkyl Chain Tool:** Drag to draw a zigzag chain of any length — the bond length stays fixed, the chain axis snaps to 15° steps, and the number of carbons that will be added (`n`) is previewed next to the cursor.
|
|
745
746
|
* **Charges & Radicals:** Quickly set formal charges (`+`/`-`) and radicals (`.`) on any atom using keyboard shortcuts or the toolbar.
|
|
746
747
|
* **Full Element Support:** Select any element from a built-in periodic table dialog.
|
|
747
748
|
* **Clipboard Operations:** Full support for `Cut` (Ctrl+X), `Copy` (Ctrl+C), and `Paste` (Ctrl+V) for molecular fragments.
|
|
@@ -39,6 +39,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
|
|
|
39
39
|
|
|
40
40
|
* **Intuitive Operations:** Easily add, edit, and delete atoms and bonds with simple mouse controls. Left-click and drag to create, and right-click to delete.
|
|
41
41
|
* **Advanced Templates:** Place templates for benzene or 3- to 9-membered rings with a live preview. Features intelligent logic to automatically adjust the double-bond configuration when fusing a benzene ring to an existing structure.
|
|
42
|
+
* **Alkyl Chain Tool:** Drag to draw a zigzag chain of any length — the bond length stays fixed, the chain axis snaps to 15° steps, and the number of carbons that will be added (`n`) is previewed next to the cursor.
|
|
42
43
|
* **Charges & Radicals:** Quickly set formal charges (`+`/`-`) and radicals (`.`) on any atom using keyboard shortcuts or the toolbar.
|
|
43
44
|
* **Full Element Support:** Select any element from a built-in periodic table dialog.
|
|
44
45
|
* **Clipboard Operations:** Full support for `Cut` (Ctrl+X), `Copy` (Ctrl+C), and `Paste` (Ctrl+V) for molecular fragments.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.6.1
|
|
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
|
|
@@ -742,6 +742,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
|
|
|
742
742
|
|
|
743
743
|
* **Intuitive Operations:** Easily add, edit, and delete atoms and bonds with simple mouse controls. Left-click and drag to create, and right-click to delete.
|
|
744
744
|
* **Advanced Templates:** Place templates for benzene or 3- to 9-membered rings with a live preview. Features intelligent logic to automatically adjust the double-bond configuration when fusing a benzene ring to an existing structure.
|
|
745
|
+
* **Alkyl Chain Tool:** Drag to draw a zigzag chain of any length — the bond length stays fixed, the chain axis snaps to 15° steps, and the number of carbons that will be added (`n`) is previewed next to the cursor.
|
|
745
746
|
* **Charges & Radicals:** Quickly set formal charges (`+`/`-`) and radicals (`.`) on any atom using keyboard shortcuts or the toolbar.
|
|
746
747
|
* **Full Element Support:** Select any element from a built-in periodic table dialog.
|
|
747
748
|
* **Clipboard Operations:** Full support for `Cut` (Ctrl+X), `Copy` (Ctrl+C), and `Paste` (Ctrl+V) for molecular fragments.
|
|
@@ -34,6 +34,7 @@ src/moleditpy_linux/ui/base_picking_dialog.py
|
|
|
34
34
|
src/moleditpy_linux/ui/bond_item.py
|
|
35
35
|
src/moleditpy_linux/ui/bond_length_dialog.py
|
|
36
36
|
src/moleditpy_linux/ui/calculation_worker.py
|
|
37
|
+
src/moleditpy_linux/ui/chain_mixin.py
|
|
37
38
|
src/moleditpy_linux/ui/color_settings_dialog.py
|
|
38
39
|
src/moleditpy_linux/ui/compute_logic.py
|
|
39
40
|
src/moleditpy_linux/ui/constrained_optimization_dialog.py
|
|
@@ -34,7 +34,6 @@ from PyQt6.QtWidgets import (
|
|
|
34
34
|
from ..utils.constants import (
|
|
35
35
|
ATOM_RADIUS,
|
|
36
36
|
CPK_COLORS,
|
|
37
|
-
DESIRED_ATOM_PIXEL_RADIUS,
|
|
38
37
|
FONT_FAMILY,
|
|
39
38
|
FONT_WEIGHT_BOLD,
|
|
40
39
|
)
|
|
@@ -332,7 +331,13 @@ class AtomItem(QGraphicsItem):
|
|
|
332
331
|
return path
|
|
333
332
|
|
|
334
333
|
def shape(self) -> QPainterPath:
|
|
335
|
-
"""Define the shape of the atom item for collision detection.
|
|
334
|
+
"""Define the shape of the atom item for collision detection.
|
|
335
|
+
|
|
336
|
+
The hit radius is derived from the ``bond_snapping_distance_2d``
|
|
337
|
+
setting so that hover highlight matches the snapping / keyboard
|
|
338
|
+
instant-switch zone exactly. The radius is expressed in screen
|
|
339
|
+
pixels and divided by the current view scale.
|
|
340
|
+
"""
|
|
336
341
|
scene = self.scene()
|
|
337
342
|
if not scene or not scene.views():
|
|
338
343
|
path = QPainterPath()
|
|
@@ -342,8 +347,16 @@ class AtomItem(QGraphicsItem):
|
|
|
342
347
|
|
|
343
348
|
view = scene.views()[0]
|
|
344
349
|
scale = view.transform().m11()
|
|
350
|
+
if scale <= 0.0:
|
|
351
|
+
scale = 1.0
|
|
345
352
|
|
|
346
|
-
|
|
353
|
+
# Use the same setting that find_atom_near() uses for snapping,
|
|
354
|
+
# so hover highlight and snap zones are identical.
|
|
355
|
+
if hasattr(scene, "get_setting"):
|
|
356
|
+
hit_px = scene.get_setting("bond_snapping_distance_2d", 14.0)
|
|
357
|
+
else:
|
|
358
|
+
hit_px = 14.0
|
|
359
|
+
scene_radius = hit_px / scale
|
|
347
360
|
|
|
348
361
|
path = QPainterPath()
|
|
349
362
|
path.addEllipse(QPointF(0, 0), scene_radius, scene_radius)
|
|
@@ -36,7 +36,6 @@ from PyQt6.QtWidgets import (
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
from ..utils.constants import (
|
|
39
|
-
DESIRED_BOND_PIXEL_WIDTH,
|
|
40
39
|
EZ_LABEL_BOX_SIZE,
|
|
41
40
|
EZ_LABEL_MARGIN,
|
|
42
41
|
EZ_LABEL_TEXT_OUTLINE,
|
|
@@ -293,10 +292,25 @@ class BondItem(QGraphicsItem):
|
|
|
293
292
|
path.moveTo(line.p1())
|
|
294
293
|
path.lineTo(line.p2())
|
|
295
294
|
|
|
296
|
-
#
|
|
297
|
-
|
|
295
|
+
# Use the exact same setting as atom hover highlight and snapping, zoom-aware.
|
|
296
|
+
scene = self.scene()
|
|
297
|
+
if scene and scene.views():
|
|
298
|
+
view = scene.views()[0]
|
|
299
|
+
scale = view.transform().m11()
|
|
300
|
+
if scale <= 0.0:
|
|
301
|
+
scale = 1.0
|
|
302
|
+
if hasattr(scene, "get_setting"):
|
|
303
|
+
hit_px = scene.get_setting("bond_snapping_distance_2d", 14.0)
|
|
304
|
+
else:
|
|
305
|
+
hit_px = 14.0
|
|
306
|
+
# width is diameter, so 2 * radius
|
|
307
|
+
scene_width = (hit_px * 2) / scale
|
|
308
|
+
else:
|
|
309
|
+
scene_width = 28.0
|
|
310
|
+
|
|
311
|
+
# Stroke it to give it some width
|
|
298
312
|
stroker = QPainterPathStroker()
|
|
299
|
-
stroker.setWidth(
|
|
313
|
+
stroker.setWidth(scene_width)
|
|
300
314
|
path = stroker.createStroke(path)
|
|
301
315
|
|
|
302
316
|
# If there's an E/Z label, add its rect to the selection shape
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
from typing import Any, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
from PyQt6.QtCore import QPointF
|
|
19
|
+
|
|
20
|
+
from .atom_item import AtomItem
|
|
21
|
+
from ..utils.constants import DEFAULT_BOND_LENGTH
|
|
22
|
+
|
|
23
|
+
# Half of the 120 deg zigzag angle, measured from the chain axis.
|
|
24
|
+
CHAIN_HALF_ANGLE_DEG = 30.0
|
|
25
|
+
CHAIN_ANGLE_SNAP_DEG = 15.0
|
|
26
|
+
MAX_CHAIN_LENGTH = 100
|
|
27
|
+
# Reject an end snap that would collapse the tail bond onto its neighbour.
|
|
28
|
+
MIN_TAIL_BOND_RATIO = 0.4
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ChainMixin:
|
|
32
|
+
"""Drag-to-draw tool that grows a fixed-bond-length zigzag alkyl chain."""
|
|
33
|
+
|
|
34
|
+
add_molecule_fragment: Any
|
|
35
|
+
current_atom_symbol: Any
|
|
36
|
+
find_atom_near: Any
|
|
37
|
+
get_setting: Any
|
|
38
|
+
template_preview: Any
|
|
39
|
+
|
|
40
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
41
|
+
self.chain_anchor: Optional[QPointF] = None
|
|
42
|
+
self.chain_start_atom: Optional[AtomItem] = None
|
|
43
|
+
self.chain_end_atom: Optional[AtomItem] = None
|
|
44
|
+
self.chain_points: List[QPointF] = []
|
|
45
|
+
self.chain_active: bool = False
|
|
46
|
+
super().__init__(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
def begin_chain(self, pos: QPointF, start_atom: Optional[AtomItem] = None) -> None:
|
|
49
|
+
"""Anchor a new chain drag at ``pos`` (or at ``start_atom`` if given)."""
|
|
50
|
+
self.chain_start_atom = start_atom
|
|
51
|
+
self.chain_end_atom = None
|
|
52
|
+
self.chain_anchor = start_atom.pos() if start_atom is not None else QPointF(pos)
|
|
53
|
+
self.chain_points = []
|
|
54
|
+
self.chain_active = True
|
|
55
|
+
|
|
56
|
+
def chain_geometry(
|
|
57
|
+
self, cursor: QPointF
|
|
58
|
+
) -> Tuple[List[QPointF], Optional[AtomItem]]:
|
|
59
|
+
"""Zigzag for this cursor, with the last vertex pulled onto a nearby atom.
|
|
60
|
+
|
|
61
|
+
Every other bond keeps the fixed length, so the tail bond absorbs the
|
|
62
|
+
whole difference and may end up longer or shorter. Both ends of the drag
|
|
63
|
+
use the shared 2D bond snapping distance, as bond drawing does.
|
|
64
|
+
"""
|
|
65
|
+
if self.chain_anchor is None:
|
|
66
|
+
return [], None
|
|
67
|
+
|
|
68
|
+
points = self.calculate_chain_points(self.chain_anchor, cursor)
|
|
69
|
+
if len(points) < 2:
|
|
70
|
+
return points, None
|
|
71
|
+
|
|
72
|
+
snap_dist = self.get_setting("bond_snapping_distance_2d", 14.0)
|
|
73
|
+
target = self.find_atom_near(cursor, tol=snap_dist)
|
|
74
|
+
if target is None or target is self.chain_start_atom:
|
|
75
|
+
return points, None
|
|
76
|
+
|
|
77
|
+
end = target.pos()
|
|
78
|
+
prev = points[-2]
|
|
79
|
+
tail = math.hypot(end.x() - prev.x(), end.y() - prev.y())
|
|
80
|
+
if tail < MIN_TAIL_BOND_RATIO * DEFAULT_BOND_LENGTH:
|
|
81
|
+
return points, None
|
|
82
|
+
|
|
83
|
+
points[-1] = end
|
|
84
|
+
return points, target
|
|
85
|
+
|
|
86
|
+
def calculate_chain_points(self, anchor: QPointF, cursor: QPointF) -> List[QPointF]:
|
|
87
|
+
"""Return the zigzag vertices from ``anchor`` toward ``cursor``.
|
|
88
|
+
|
|
89
|
+
The chain axis is snapped to 15 deg steps, every bond keeps the standard
|
|
90
|
+
2D bond length, and the number of bonds follows the drag distance
|
|
91
|
+
projected onto the axis.
|
|
92
|
+
"""
|
|
93
|
+
vx = cursor.x() - anchor.x()
|
|
94
|
+
vy = cursor.y() - anchor.y()
|
|
95
|
+
|
|
96
|
+
snap = math.radians(CHAIN_ANGLE_SNAP_DEG)
|
|
97
|
+
axis_angle = round(math.atan2(vy, vx) / snap) * snap
|
|
98
|
+
ax, ay = math.cos(axis_angle), math.sin(axis_angle)
|
|
99
|
+
|
|
100
|
+
half = math.radians(CHAIN_HALF_ANGLE_DEG)
|
|
101
|
+
step = DEFAULT_BOND_LENGTH * math.cos(half)
|
|
102
|
+
offset = DEFAULT_BOND_LENGTH * math.sin(half)
|
|
103
|
+
|
|
104
|
+
count = int(round((vx * ax + vy * ay) / step))
|
|
105
|
+
if count < 1:
|
|
106
|
+
return []
|
|
107
|
+
count = min(count, MAX_CHAIN_LENGTH)
|
|
108
|
+
|
|
109
|
+
# Zigzag bulges toward the side of the axis the cursor drifted to.
|
|
110
|
+
side = -1.0 if (ax * vy - ay * vx) < 0 else 1.0
|
|
111
|
+
px, py = -ay * offset * side, ax * offset * side
|
|
112
|
+
|
|
113
|
+
points = [QPointF(anchor)]
|
|
114
|
+
for k in range(1, count + 1):
|
|
115
|
+
wobble = 1.0 if k % 2 else 0.0
|
|
116
|
+
points.append(
|
|
117
|
+
QPointF(
|
|
118
|
+
anchor.x() + ax * step * k + px * wobble,
|
|
119
|
+
anchor.y() + ay * step * k + py * wobble,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return points
|
|
123
|
+
|
|
124
|
+
def update_chain_preview(self, pos: QPointF) -> None:
|
|
125
|
+
"""Refresh the ghost chain and the repeat-count label near the cursor."""
|
|
126
|
+
if not self.chain_active or self.chain_anchor is None:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
self.chain_points, self.chain_end_atom = self.chain_geometry(pos)
|
|
130
|
+
if len(self.chain_points) < 2:
|
|
131
|
+
self.template_preview.hide()
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
self.template_preview.set_chain_geometry(
|
|
135
|
+
self.chain_points,
|
|
136
|
+
f"n = {self.new_atom_count()}",
|
|
137
|
+
QPointF(pos.x() + 16, pos.y() - 12),
|
|
138
|
+
)
|
|
139
|
+
self.template_preview.show()
|
|
140
|
+
|
|
141
|
+
def new_atom_count(self) -> int:
|
|
142
|
+
"""Atoms the previewed chain would add; reused end atoms do not count."""
|
|
143
|
+
if not self.chain_points:
|
|
144
|
+
return 0
|
|
145
|
+
reused = sum(
|
|
146
|
+
1
|
|
147
|
+
for atom in (self.chain_start_atom, self.chain_end_atom)
|
|
148
|
+
if atom is not None
|
|
149
|
+
)
|
|
150
|
+
return len(self.chain_points) - reused
|
|
151
|
+
|
|
152
|
+
def clear_chain_preview(self) -> None:
|
|
153
|
+
"""Hide the ghost chain and drop the drag state."""
|
|
154
|
+
self.chain_anchor = None
|
|
155
|
+
self.chain_start_atom = None
|
|
156
|
+
self.chain_end_atom = None
|
|
157
|
+
self.chain_points = []
|
|
158
|
+
self.chain_active = False
|
|
159
|
+
self.template_preview.hide()
|
|
160
|
+
|
|
161
|
+
def commit_chain(self, pos: QPointF) -> bool:
|
|
162
|
+
"""Materialize the previewed chain as atoms and bonds. True if anything was added."""
|
|
163
|
+
if self.chain_anchor is None:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
points, end_atom = self.chain_geometry(pos)
|
|
167
|
+
if len(points) < 2:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
bonds_info = [(i, i + 1, 1) for i in range(len(points) - 1)]
|
|
171
|
+
# Naming both reused atoms fuses them even with template fusing turned off.
|
|
172
|
+
existing = [
|
|
173
|
+
atom for atom in (self.chain_start_atom, end_atom) if atom is not None
|
|
174
|
+
]
|
|
175
|
+
self.add_molecule_fragment(
|
|
176
|
+
points, bonds_info, existing_items=existing, symbol=self.current_atom_symbol
|
|
177
|
+
)
|
|
178
|
+
return True
|
|
@@ -873,6 +873,17 @@ class MainInitManager:
|
|
|
873
873
|
self.tool_group.addAction(action) # type: ignore[union-attr]
|
|
874
874
|
toolbar.addSeparator()
|
|
875
875
|
|
|
876
|
+
def _add_chain_action(self, toolbar: QToolBar) -> None:
|
|
877
|
+
"""Add the drag-to-draw alkyl chain action."""
|
|
878
|
+
action = QAction(self.host)
|
|
879
|
+
action.setIcon(self._create_chain_icon())
|
|
880
|
+
action.setToolTip("Alkyl Chain — drag to draw a zigzag chain")
|
|
881
|
+
action.setCheckable(True)
|
|
882
|
+
action.triggered.connect(lambda checked: self.host.ui_manager.set_mode("chain"))
|
|
883
|
+
self.mode_actions["chain"] = action
|
|
884
|
+
toolbar.addAction(action)
|
|
885
|
+
self.tool_group.addAction(action) # type: ignore[union-attr]
|
|
886
|
+
|
|
876
887
|
def _add_charge_radical_actions(self, toolbar: QToolBar) -> None:
|
|
877
888
|
"""Add charge and radical modification actions."""
|
|
878
889
|
ops = [
|
|
@@ -910,6 +921,8 @@ class MainInitManager:
|
|
|
910
921
|
toolbar_bottom.addAction(action)
|
|
911
922
|
self.tool_group.addAction(action) # type: ignore[union-attr]
|
|
912
923
|
|
|
924
|
+
self._add_chain_action(toolbar_bottom)
|
|
925
|
+
|
|
913
926
|
user_action = QAction("USER", self.host)
|
|
914
927
|
user_action.setCheckable(True)
|
|
915
928
|
user_action.setToolTip("Open User Templates Dialog")
|
|
@@ -1039,6 +1052,57 @@ class MainInitManager:
|
|
|
1039
1052
|
painter.end()
|
|
1040
1053
|
return QIcon(pixmap)
|
|
1041
1054
|
|
|
1055
|
+
def _create_chain_icon(self, size: int = 32) -> QIcon:
|
|
1056
|
+
"""Generate the -(-)n- repeat-unit icon for the alkyl chain tool."""
|
|
1057
|
+
fg = self._get_icon_foreground_color()
|
|
1058
|
+
pixmap = QPixmap(size, size)
|
|
1059
|
+
pixmap.fill(Qt.GlobalColor.transparent)
|
|
1060
|
+
painter = QPainter(pixmap)
|
|
1061
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
1062
|
+
painter.setPen(QPen(fg, max(1.8, size / 16.0)))
|
|
1063
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
1064
|
+
|
|
1065
|
+
cy = size * 0.375
|
|
1066
|
+
amp = size * 0.17
|
|
1067
|
+
# Backbone zigzag spans the full width so the repeat brackets cross it.
|
|
1068
|
+
zig = [
|
|
1069
|
+
QPointF(1 + i * (size - 2) / 2.0, cy + (amp if i % 2 == 0 else -amp))
|
|
1070
|
+
for i in range(3)
|
|
1071
|
+
]
|
|
1072
|
+
for start, end in zip(zig, zig[1:]):
|
|
1073
|
+
painter.drawLine(start, end)
|
|
1074
|
+
|
|
1075
|
+
painter.setPen(QPen(fg, max(1.5, size / 20.0)))
|
|
1076
|
+
bracket_h = size * 0.55
|
|
1077
|
+
bracket_w = size * 0.19
|
|
1078
|
+
inset = size * 0.13
|
|
1079
|
+
painter.drawArc(
|
|
1080
|
+
QRectF(inset, cy - bracket_h / 2, bracket_w, bracket_h), 118 * 16, 124 * 16
|
|
1081
|
+
)
|
|
1082
|
+
painter.drawArc(
|
|
1083
|
+
QRectF(size - inset - bracket_w, cy - bracket_h / 2, bracket_w, bracket_h),
|
|
1084
|
+
-62 * 16,
|
|
1085
|
+
124 * 16,
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
font = painter.font()
|
|
1089
|
+
font.setPixelSize(max(8, int(size * 0.30)))
|
|
1090
|
+
font.setBold(True)
|
|
1091
|
+
painter.setFont(font)
|
|
1092
|
+
painter.drawText(
|
|
1093
|
+
QRectF(
|
|
1094
|
+
size - size * 0.26,
|
|
1095
|
+
cy + bracket_h / 2 - size * 0.06,
|
|
1096
|
+
size * 0.24,
|
|
1097
|
+
size * 0.36,
|
|
1098
|
+
),
|
|
1099
|
+
Qt.AlignmentFlag.AlignCenter,
|
|
1100
|
+
"n",
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
painter.end()
|
|
1104
|
+
return QIcon(pixmap)
|
|
1105
|
+
|
|
1042
1106
|
def _create_template_icon(
|
|
1043
1107
|
self, n: int, is_benzene: bool = False, size: int = 32
|
|
1044
1108
|
) -> QIcon:
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/molecular_scene_handler.py
RENAMED
|
@@ -856,7 +856,10 @@ class KeyboardMixin:
|
|
|
856
856
|
return
|
|
857
857
|
|
|
858
858
|
view = self.views()[0]
|
|
859
|
-
|
|
859
|
+
# mapToScene expects viewport coordinates. Mapping to the view widget directly
|
|
860
|
+
# includes the widget's frame margins, introducing a cursor offset that gets
|
|
861
|
+
# magnified when zooming.
|
|
862
|
+
cursor_pos = view.mapToScene(view.viewport().mapFromGlobal(QCursor.pos()))
|
|
860
863
|
transform = view.transform()
|
|
861
864
|
key = event.key()
|
|
862
865
|
modifiers = event.modifiers()
|
|
@@ -1587,9 +1590,22 @@ class SceneQueryMixin:
|
|
|
1587
1590
|
return False
|
|
1588
1591
|
|
|
1589
1592
|
def find_atom_near(self, pos: Any, tol: float = 14.0) -> Any:
|
|
1590
|
-
"""Return the AtomItem within tolerance distance of pos, or None.
|
|
1593
|
+
"""Return the AtomItem within tolerance distance of pos, or None.
|
|
1594
|
+
|
|
1595
|
+
``tol`` is specified in **screen pixels**. It is divided by the
|
|
1596
|
+
current view scale so that the effective snap radius stays constant
|
|
1597
|
+
on screen regardless of zoom level — matching the behaviour of
|
|
1598
|
+
``AtomItem.shape()`` (hover highlight).
|
|
1599
|
+
"""
|
|
1591
1600
|
if pos is None:
|
|
1592
1601
|
return None
|
|
1602
|
+
# Convert screen-pixel tolerance to scene units (zoom-aware).
|
|
1603
|
+
if self.views():
|
|
1604
|
+
xform = self.views()[0].transform()
|
|
1605
|
+
if xform is not None:
|
|
1606
|
+
scale = xform.m11()
|
|
1607
|
+
if scale > 0:
|
|
1608
|
+
tol = tol / scale
|
|
1593
1609
|
# Create a small search rectangle around the position
|
|
1594
1610
|
search_rect = QRectF(pos.x() - tol, pos.y() - tol, 2 * tol, 2 * tol)
|
|
1595
1611
|
nearby_items = self.items(search_rect)
|
|
@@ -36,9 +36,12 @@ from .template_preview_item import TemplatePreviewItem
|
|
|
36
36
|
from ..utils.sip_isdeleted_safe import sip_isdeleted_safe
|
|
37
37
|
|
|
38
38
|
from .molecular_scene_handler import TemplateMixin, KeyboardMixin, SceneQueryMixin
|
|
39
|
+
from .chain_mixin import ChainMixin
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
class MoleculeScene(
|
|
42
|
+
class MoleculeScene(
|
|
43
|
+
ChainMixin, TemplateMixin, KeyboardMixin, SceneQueryMixin, QGraphicsScene
|
|
44
|
+
):
|
|
42
45
|
"""Central QGraphicsScene that owns all 2D atom and bond items."""
|
|
43
46
|
|
|
44
47
|
def __init__(self, data: MolecularData, window: MainWindow) -> None:
|
|
@@ -433,6 +436,15 @@ class MoleculeScene(TemplateMixin, KeyboardMixin, SceneQueryMixin, QGraphicsScen
|
|
|
433
436
|
event.accept()
|
|
434
437
|
return # Complete right-click processing and prevent proceeding to left-click logic
|
|
435
438
|
|
|
439
|
+
if self.mode == "chain":
|
|
440
|
+
self.clearSelection()
|
|
441
|
+
snap_dist = self.get_setting("bond_snapping_distance_2d", 14.0)
|
|
442
|
+
self.begin_chain(
|
|
443
|
+
self.press_pos, self.find_atom_near(self.press_pos, tol=snap_dist)
|
|
444
|
+
)
|
|
445
|
+
event.accept()
|
|
446
|
+
return
|
|
447
|
+
|
|
436
448
|
if self.mode.startswith("template"):
|
|
437
449
|
self.clearSelection() # In template mode, no selection is made; only the click position is recorded.
|
|
438
450
|
return
|
|
@@ -487,6 +499,10 @@ class MoleculeScene(TemplateMixin, KeyboardMixin, SceneQueryMixin, QGraphicsScen
|
|
|
487
499
|
if self.mode.startswith("template"):
|
|
488
500
|
self.update_template_preview(event.scenePos())
|
|
489
501
|
|
|
502
|
+
if self.mode == "chain" and self.chain_active:
|
|
503
|
+
self.update_chain_preview(event.scenePos())
|
|
504
|
+
return
|
|
505
|
+
|
|
490
506
|
if not self.mouse_moved_since_press and self.press_pos:
|
|
491
507
|
if (
|
|
492
508
|
event.scenePos() - self.press_pos
|
|
@@ -544,6 +560,20 @@ class MoleculeScene(TemplateMixin, KeyboardMixin, SceneQueryMixin, QGraphicsScen
|
|
|
544
560
|
|
|
545
561
|
self.temp_line = None
|
|
546
562
|
|
|
563
|
+
if self.mode == "chain":
|
|
564
|
+
committed = False
|
|
565
|
+
if self.chain_active:
|
|
566
|
+
committed = self.commit_chain(end_pos)
|
|
567
|
+
self.clear_chain_preview()
|
|
568
|
+
self.start_atom = None
|
|
569
|
+
self.start_pos = None
|
|
570
|
+
self.press_pos = None
|
|
571
|
+
if committed:
|
|
572
|
+
self.data_changed_in_event = True
|
|
573
|
+
self.update_all_items()
|
|
574
|
+
self.window.edit_actions_manager.push_undo_state()
|
|
575
|
+
return
|
|
576
|
+
|
|
547
577
|
if self.mode.startswith("template") and is_click:
|
|
548
578
|
if self.template_context and self.template_context.get("points"):
|
|
549
579
|
context = self.template_context
|
|
@@ -972,8 +1002,8 @@ class MoleculeScene(TemplateMixin, KeyboardMixin, SceneQueryMixin, QGraphicsScen
|
|
|
972
1002
|
# Find the active view for this scene
|
|
973
1003
|
for view in self.views():
|
|
974
1004
|
if view.isVisible():
|
|
975
|
-
# Map global cursor position to scene coordinates
|
|
976
|
-
local_pos = view.mapFromGlobal(global_pos)
|
|
1005
|
+
# Map global cursor position to scene coordinates (via viewport)
|
|
1006
|
+
local_pos = view.viewport().mapFromGlobal(global_pos)
|
|
977
1007
|
scene_pos = view.mapToScene(local_pos)
|
|
978
1008
|
|
|
979
1009
|
# If the mouse is within the viewport, trigger the preview update
|
|
@@ -198,7 +198,10 @@ class Settings2DTab(SettingsTabBase):
|
|
|
198
198
|
self._create_slider(5, 50, 1.0, is_int=True)
|
|
199
199
|
)
|
|
200
200
|
self.bond_snapping_distance_2d_slider.setToolTip(
|
|
201
|
-
"The distance in pixels within which
|
|
201
|
+
"The distance in screen pixels (zoom-independent) within which:\n"
|
|
202
|
+
"• Drawing a bond snaps to an existing atom\n"
|
|
203
|
+
"• Keyboard shortcuts instantly edit the atom under cursor\n"
|
|
204
|
+
"• Atoms show hover highlight"
|
|
202
205
|
)
|
|
203
206
|
form_layout.addRow(
|
|
204
207
|
"Bond Snapping Distance (px):",
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/template_preview_item.py
RENAMED
|
@@ -34,6 +34,10 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
34
34
|
self.user_template_bonds: List[Any] = []
|
|
35
35
|
self.user_template_atoms: List[Any] = []
|
|
36
36
|
self.is_user_template = False
|
|
37
|
+
self.chain_points: List[QPointF] = []
|
|
38
|
+
self.chain_label = ""
|
|
39
|
+
self.chain_label_pos = QPointF()
|
|
40
|
+
self.is_chain = False
|
|
37
41
|
|
|
38
42
|
def set_geometry(self, points: list[QPointF], is_aromatic: bool = False) -> None:
|
|
39
43
|
"""Set polygon points for a standard ring template preview."""
|
|
@@ -41,6 +45,21 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
41
45
|
self.polygon = QPolygonF(points)
|
|
42
46
|
self.is_aromatic = is_aromatic
|
|
43
47
|
self.is_user_template = False
|
|
48
|
+
self.is_chain = False
|
|
49
|
+
self.update()
|
|
50
|
+
|
|
51
|
+
def set_chain_geometry(
|
|
52
|
+
self, points: list[QPointF], label: str, label_pos: QPointF
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Set the zigzag vertices and the repeat-count label for a chain preview."""
|
|
55
|
+
self.prepareGeometryChange()
|
|
56
|
+
self.chain_points = list(points)
|
|
57
|
+
self.chain_label = label
|
|
58
|
+
self.chain_label_pos = QPointF(label_pos)
|
|
59
|
+
self.is_chain = True
|
|
60
|
+
self.is_user_template = False
|
|
61
|
+
self.is_aromatic = False
|
|
62
|
+
self.polygon = QPolygonF()
|
|
44
63
|
self.update()
|
|
45
64
|
|
|
46
65
|
def set_user_template_geometry(
|
|
@@ -55,12 +74,22 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
55
74
|
self.user_template_bonds = bonds_info
|
|
56
75
|
self.user_template_atoms = atoms_data
|
|
57
76
|
self.is_user_template = True
|
|
77
|
+
self.is_chain = False
|
|
58
78
|
self.is_aromatic = False
|
|
59
79
|
self.polygon = QPolygonF()
|
|
60
80
|
self.update()
|
|
61
81
|
|
|
62
82
|
def boundingRect(self) -> QRectF:
|
|
63
83
|
"""Return the bounding rect encompassing the preview geometry."""
|
|
84
|
+
if self.is_chain and self.chain_points:
|
|
85
|
+
xs = [p.x() for p in self.chain_points] + [self.chain_label_pos.x()]
|
|
86
|
+
ys = [p.y() for p in self.chain_points] + [self.chain_label_pos.y()]
|
|
87
|
+
return QRectF(
|
|
88
|
+
min(xs) - 10,
|
|
89
|
+
min(ys) - 20,
|
|
90
|
+
max(xs) - min(xs) + 90,
|
|
91
|
+
max(ys) - min(ys) + 40,
|
|
92
|
+
)
|
|
64
93
|
if self.is_user_template and self.user_template_points:
|
|
65
94
|
# Calculate bounding rect for user template
|
|
66
95
|
min_x = min(p.x() for p in self.user_template_points)
|
|
@@ -81,11 +110,43 @@ class TemplatePreviewItem(QGraphicsItem):
|
|
|
81
110
|
"""Dispatch to the regular or user-template painter."""
|
|
82
111
|
if painter is None:
|
|
83
112
|
return
|
|
84
|
-
if self.
|
|
113
|
+
if self.is_chain:
|
|
114
|
+
self.paint_chain(painter)
|
|
115
|
+
elif self.is_user_template:
|
|
85
116
|
self.paint_user_template(painter)
|
|
86
117
|
else:
|
|
87
118
|
self.paint_regular_template(painter)
|
|
88
119
|
|
|
120
|
+
def paint_chain(self, painter: QPainter) -> None:
|
|
121
|
+
"""Paint the ghost zigzag chain plus the repeat count next to the cursor."""
|
|
122
|
+
if len(self.chain_points) < 2:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
painter.setPen(QPen(QColor(80, 80, 80, 180), 2.5))
|
|
126
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
127
|
+
for i in range(len(self.chain_points) - 1):
|
|
128
|
+
painter.drawLine(QLineF(self.chain_points[i], self.chain_points[i + 1]))
|
|
129
|
+
|
|
130
|
+
dot = QColor(80, 80, 80, 140)
|
|
131
|
+
painter.setBrush(QBrush(dot))
|
|
132
|
+
painter.setPen(QPen(dot, 1))
|
|
133
|
+
for p in self.chain_points[1:]:
|
|
134
|
+
painter.drawEllipse(p, 2.5, 2.5)
|
|
135
|
+
|
|
136
|
+
if not self.chain_label:
|
|
137
|
+
return
|
|
138
|
+
font = QFont("Arial", 11, QFont.Weight.Bold)
|
|
139
|
+
painter.setFont(font)
|
|
140
|
+
metrics = painter.fontMetrics()
|
|
141
|
+
rect = QRectF(metrics.boundingRect(self.chain_label))
|
|
142
|
+
rect.moveTopLeft(self.chain_label_pos)
|
|
143
|
+
rect = rect.adjusted(-5, -3, 5, 3)
|
|
144
|
+
painter.setPen(QPen(QColor(80, 80, 80, 180), 1))
|
|
145
|
+
painter.setBrush(QBrush(QColor(255, 255, 255, 225)))
|
|
146
|
+
painter.drawRoundedRect(rect, 4, 4)
|
|
147
|
+
painter.setPen(QPen(QColor(40, 40, 40)))
|
|
148
|
+
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, self.chain_label)
|
|
149
|
+
|
|
89
150
|
def paint_regular_template(self, painter: QPainter) -> None:
|
|
90
151
|
"""Paint a standard ring or aromatic template as a polygon outline."""
|
|
91
152
|
painter.setPen(self.pen)
|
|
@@ -62,6 +62,10 @@ class UIManager(QObject):
|
|
|
62
62
|
|
|
63
63
|
# Trigger immediate scene refresh to show/update template previews
|
|
64
64
|
self.host.init_manager.scene.refresh_mode_state()
|
|
65
|
+
# A shortcut key can switch modes mid-drag; drop the abandoned chain anchor.
|
|
66
|
+
if prev_mode == "chain" and mode_str != "chain":
|
|
67
|
+
self.host.init_manager.scene.clear_chain_preview()
|
|
68
|
+
|
|
65
69
|
# Clear ghost when leaving template mode
|
|
66
70
|
if (
|
|
67
71
|
prev_mode
|
|
@@ -75,7 +79,7 @@ class UIManager(QObject):
|
|
|
75
79
|
# Set cursor shape
|
|
76
80
|
if mode_str == "select":
|
|
77
81
|
self.host.init_manager.view_2d.setCursor(Qt.CursorShape.ArrowCursor)
|
|
78
|
-
elif mode_str.startswith(("atom", "bond", "template")):
|
|
82
|
+
elif mode_str.startswith(("atom", "bond", "template", "chain")):
|
|
79
83
|
self.host.init_manager.view_2d.setCursor(Qt.CursorShape.CrossCursor)
|
|
80
84
|
elif mode_str.startswith(("charge", "radical")):
|
|
81
85
|
self.host.init_manager.view_2d.setCursor(Qt.CursorShape.CrossCursor)
|
|
@@ -101,6 +105,14 @@ class UIManager(QObject):
|
|
|
101
105
|
)
|
|
102
106
|
self.host.init_manager.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
103
107
|
self.host.init_manager.view_2d.setMouseTracking(True)
|
|
108
|
+
elif mode_str == "chain":
|
|
109
|
+
self.host.set_scene_atom_symbol("C")
|
|
110
|
+
self.host.set_scene_bond_properties(1, 0)
|
|
111
|
+
self.host.update_status_message(
|
|
112
|
+
"Mode: Alkyl Chain (Drag to set the chain length)"
|
|
113
|
+
)
|
|
114
|
+
self.host.init_manager.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
115
|
+
self.host.init_manager.view_2d.setMouseTracking(True)
|
|
104
116
|
elif mode_str.startswith("template"):
|
|
105
117
|
if mode_str.startswith("template_user"):
|
|
106
118
|
# User template mode
|
|
@@ -113,7 +113,7 @@ class ZoomableView(QGraphicsView):
|
|
|
113
113
|
current_mode = self.scene().mode if self.scene() else "select" # type: ignore[union-attr]
|
|
114
114
|
if current_mode == "select":
|
|
115
115
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
116
|
-
elif current_mode.startswith(("atom", "bond", "template")):
|
|
116
|
+
elif current_mode.startswith(("atom", "bond", "template", "chain")):
|
|
117
117
|
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
118
118
|
elif current_mode.startswith(("charge", "radical")):
|
|
119
119
|
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/MoleditPy_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/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.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/plugins/plugin_manager_window.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/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.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/base_picking_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/bond_length_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/calculation_worker.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/color_settings_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/custom_interactor_style.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/custom_qt_interactor.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/edit_actions_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/geometry_base_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/periodic_table_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/plugin_menu_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/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.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/template_preview_view.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/translation_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/ui/user_template_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/utils/default_settings.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.5.2 → moleditpy_linux-4.6.1}/src/moleditpy_linux/utils/sip_isdeleted_safe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|