MoleditPy-linux 2.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moleditpy_linux/__init__.py +17 -0
- moleditpy_linux/__main__.py +29 -0
- moleditpy_linux/main.py +37 -0
- moleditpy_linux/modules/__init__.py +41 -0
- moleditpy_linux/modules/about_dialog.py +104 -0
- moleditpy_linux/modules/align_plane_dialog.py +292 -0
- moleditpy_linux/modules/alignment_dialog.py +272 -0
- moleditpy_linux/modules/analysis_window.py +209 -0
- moleditpy_linux/modules/angle_dialog.py +440 -0
- moleditpy_linux/modules/assets/file_icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.icns +0 -0
- moleditpy_linux/modules/assets/icon.ico +0 -0
- moleditpy_linux/modules/assets/icon.png +0 -0
- moleditpy_linux/modules/atom_item.py +395 -0
- moleditpy_linux/modules/bond_item.py +464 -0
- moleditpy_linux/modules/bond_length_dialog.py +380 -0
- moleditpy_linux/modules/calculation_worker.py +766 -0
- moleditpy_linux/modules/color_settings_dialog.py +321 -0
- moleditpy_linux/modules/constants.py +88 -0
- moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
- moleditpy_linux/modules/custom_interactor_style.py +749 -0
- moleditpy_linux/modules/custom_qt_interactor.py +102 -0
- moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
- moleditpy_linux/modules/dihedral_dialog.py +443 -0
- moleditpy_linux/modules/main_window.py +850 -0
- moleditpy_linux/modules/main_window_app_state.py +787 -0
- moleditpy_linux/modules/main_window_compute.py +1242 -0
- moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
- moleditpy_linux/modules/main_window_edit_3d.py +536 -0
- moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
- moleditpy_linux/modules/main_window_export.py +917 -0
- moleditpy_linux/modules/main_window_main_init.py +2100 -0
- moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
- moleditpy_linux/modules/main_window_project_io.py +434 -0
- moleditpy_linux/modules/main_window_string_importers.py +275 -0
- moleditpy_linux/modules/main_window_ui_manager.py +602 -0
- moleditpy_linux/modules/main_window_view_3d.py +1539 -0
- moleditpy_linux/modules/main_window_view_loaders.py +355 -0
- moleditpy_linux/modules/mirror_dialog.py +122 -0
- moleditpy_linux/modules/molecular_data.py +302 -0
- moleditpy_linux/modules/molecule_scene.py +2000 -0
- moleditpy_linux/modules/move_group_dialog.py +600 -0
- moleditpy_linux/modules/periodic_table_dialog.py +84 -0
- moleditpy_linux/modules/planarize_dialog.py +220 -0
- moleditpy_linux/modules/plugin_interface.py +215 -0
- moleditpy_linux/modules/plugin_manager.py +473 -0
- moleditpy_linux/modules/plugin_manager_window.py +274 -0
- moleditpy_linux/modules/settings_dialog.py +1503 -0
- moleditpy_linux/modules/template_preview_item.py +157 -0
- moleditpy_linux/modules/template_preview_view.py +74 -0
- moleditpy_linux/modules/translation_dialog.py +364 -0
- moleditpy_linux/modules/user_template_dialog.py +692 -0
- moleditpy_linux/modules/zoomable_view.py +129 -0
- moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
- moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
- moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
- moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
- moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
- moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,395 @@
|
|
|
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 PyQt6.QtWidgets import QGraphicsItem
|
|
14
|
+
|
|
15
|
+
from PyQt6.QtGui import (
|
|
16
|
+
QPen, QBrush, QColor, QFont, QPainterPath, QFontMetricsF, QPainter
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from PyQt6.QtCore import (
|
|
20
|
+
Qt, QPointF, QRectF
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from .constants import (
|
|
25
|
+
ATOM_RADIUS, DESIRED_ATOM_PIXEL_RADIUS,
|
|
26
|
+
FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
|
|
27
|
+
CPK_COLORS,
|
|
28
|
+
)
|
|
29
|
+
except Exception:
|
|
30
|
+
from modules.constants import (
|
|
31
|
+
ATOM_RADIUS, DESIRED_ATOM_PIXEL_RADIUS,
|
|
32
|
+
FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
|
|
33
|
+
CPK_COLORS,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from . import sip_isdeleted_safe
|
|
38
|
+
except Exception:
|
|
39
|
+
from modules import sip_isdeleted_safe
|
|
40
|
+
|
|
41
|
+
class AtomItem(QGraphicsItem):
|
|
42
|
+
def __init__(self, atom_id, symbol, pos, charge=0, radical=0):
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.atom_id, self.symbol, self.charge, self.radical, self.bonds, self.chiral_label = atom_id, symbol, charge, radical, [], None
|
|
45
|
+
self.setPos(pos)
|
|
46
|
+
self.implicit_h_count = 0
|
|
47
|
+
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
|
48
|
+
self.setZValue(1)
|
|
49
|
+
self.update_style()
|
|
50
|
+
self.setAcceptHoverEvents(True)
|
|
51
|
+
self.hovered = False
|
|
52
|
+
self.has_problem = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def update_style(self):
|
|
56
|
+
# Allow updating font preference dynamically
|
|
57
|
+
font_size = 20
|
|
58
|
+
try:
|
|
59
|
+
if self.scene() and self.scene().views():
|
|
60
|
+
win = self.scene().views()[0].window()
|
|
61
|
+
if win and hasattr(win, 'settings'):
|
|
62
|
+
font_size = win.settings.get('atom_font_size_2d', 20)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
self.font = QFont(FONT_FAMILY, font_size, FONT_WEIGHT_BOLD)
|
|
66
|
+
self.prepareGeometryChange()
|
|
67
|
+
|
|
68
|
+
self.is_visible = not (self.symbol == 'C' and len(self.bonds) > 0 and self.charge == 0 and self.radical == 0)
|
|
69
|
+
self.update()
|
|
70
|
+
|
|
71
|
+
def boundingRect(self):
|
|
72
|
+
# --- paint()メソッドと完全に同じロジックでテキストの位置とサイズを計算 ---
|
|
73
|
+
# Get dynamic font size
|
|
74
|
+
font_size = 20
|
|
75
|
+
try:
|
|
76
|
+
if self.scene() and self.scene().views():
|
|
77
|
+
win = self.scene().views()[0].window()
|
|
78
|
+
if win and hasattr(win, 'settings'):
|
|
79
|
+
font_size = win.settings.get('atom_font_size_2d', 20)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
font = QFont(FONT_FAMILY, font_size, FONT_WEIGHT_BOLD)
|
|
83
|
+
fm = QFontMetricsF(font)
|
|
84
|
+
|
|
85
|
+
hydrogen_part = ""
|
|
86
|
+
if self.implicit_h_count > 0:
|
|
87
|
+
is_skeletal_carbon = (self.symbol == 'C' and
|
|
88
|
+
self.charge == 0 and
|
|
89
|
+
self.radical == 0 and
|
|
90
|
+
len(self.bonds) > 0)
|
|
91
|
+
if not is_skeletal_carbon:
|
|
92
|
+
hydrogen_part = "H"
|
|
93
|
+
if self.implicit_h_count > 1:
|
|
94
|
+
subscript_map = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
|
|
95
|
+
hydrogen_part += str(self.implicit_h_count).translate(subscript_map)
|
|
96
|
+
|
|
97
|
+
flip_text = False
|
|
98
|
+
if hydrogen_part and self.bonds:
|
|
99
|
+
my_pos_x = self.pos().x()
|
|
100
|
+
total_dx = 0.0
|
|
101
|
+
# Defensive: some bonds may have missing atom references (None) or C++ wrappers
|
|
102
|
+
# that have been deleted. Iterate and accumulate only valid partner positions.
|
|
103
|
+
for b in self.bonds:
|
|
104
|
+
# partner is the atom at the other end of the bond
|
|
105
|
+
partner = b.atom2 if b.atom1 is self else b.atom1
|
|
106
|
+
try:
|
|
107
|
+
if partner is None:
|
|
108
|
+
continue
|
|
109
|
+
# If SIP reports the wrapper as deleted, skip it
|
|
110
|
+
if sip_isdeleted_safe(partner):
|
|
111
|
+
continue
|
|
112
|
+
partner_pos = partner.pos()
|
|
113
|
+
if partner_pos is None:
|
|
114
|
+
continue
|
|
115
|
+
total_dx += partner_pos.x() - my_pos_x
|
|
116
|
+
except Exception:
|
|
117
|
+
# Skip any bond that raises while inspecting; keep UI tolerant
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if total_dx > 0:
|
|
121
|
+
flip_text = True
|
|
122
|
+
|
|
123
|
+
if flip_text:
|
|
124
|
+
display_text = hydrogen_part + self.symbol
|
|
125
|
+
else:
|
|
126
|
+
display_text = self.symbol + hydrogen_part
|
|
127
|
+
|
|
128
|
+
text_rect = fm.boundingRect(display_text)
|
|
129
|
+
text_rect.adjust(-2, -2, 2, 2)
|
|
130
|
+
if hydrogen_part:
|
|
131
|
+
symbol_rect = fm.boundingRect(self.symbol)
|
|
132
|
+
if flip_text:
|
|
133
|
+
offset_x = symbol_rect.width() // 2
|
|
134
|
+
text_rect.moveTo(offset_x - text_rect.width(), -text_rect.height() / 2)
|
|
135
|
+
else:
|
|
136
|
+
offset_x = -symbol_rect.width() // 2
|
|
137
|
+
text_rect.moveTo(offset_x, -text_rect.height() / 2)
|
|
138
|
+
else:
|
|
139
|
+
text_rect.moveCenter(QPointF(0, 0))
|
|
140
|
+
|
|
141
|
+
# 1. paint()で描画される背景の矩形(bg_rect)を計算する
|
|
142
|
+
bg_rect = text_rect.adjusted(-5, -8, 5, 8)
|
|
143
|
+
|
|
144
|
+
# 2. このbg_rectを基準として全体の描画領域を構築する
|
|
145
|
+
full_visual_rect = QRectF(bg_rect)
|
|
146
|
+
|
|
147
|
+
# 電荷記号の領域を計算に含める
|
|
148
|
+
if self.charge != 0:
|
|
149
|
+
# Chemical convention: single charge as "+"/"-", multiple as "2+"/"2-"
|
|
150
|
+
if self.charge == 1:
|
|
151
|
+
charge_str = "+"
|
|
152
|
+
elif self.charge == -1:
|
|
153
|
+
charge_str = "-"
|
|
154
|
+
else:
|
|
155
|
+
sign = '+' if self.charge > 0 else '-'
|
|
156
|
+
charge_str = f"{abs(self.charge)}{sign}"
|
|
157
|
+
charge_font = QFont("Arial", 12, QFont.Weight.Bold)
|
|
158
|
+
charge_fm = QFontMetricsF(charge_font)
|
|
159
|
+
charge_rect = charge_fm.boundingRect(charge_str)
|
|
160
|
+
|
|
161
|
+
if flip_text:
|
|
162
|
+
charge_pos = QPointF(text_rect.left() - charge_rect.width() - 2, text_rect.top())
|
|
163
|
+
else:
|
|
164
|
+
charge_pos = QPointF(text_rect.right() + 2, text_rect.top())
|
|
165
|
+
charge_rect.moveTopLeft(charge_pos)
|
|
166
|
+
full_visual_rect = full_visual_rect.united(charge_rect)
|
|
167
|
+
|
|
168
|
+
# ラジカル記号の領域を計算に含める
|
|
169
|
+
if self.radical > 0:
|
|
170
|
+
radical_area = QRectF(text_rect.center().x() - 8, text_rect.top() - 8, 16, 8)
|
|
171
|
+
full_visual_rect = full_visual_rect.united(radical_area)
|
|
172
|
+
|
|
173
|
+
# 3. 選択ハイライト等のための最終的なマージンを追加する
|
|
174
|
+
return full_visual_rect.adjusted(-3, -3, 3, 3)
|
|
175
|
+
|
|
176
|
+
def shape(self):
|
|
177
|
+
scene = self.scene()
|
|
178
|
+
if not scene or not scene.views():
|
|
179
|
+
path = QPainterPath()
|
|
180
|
+
hit_r = max(4.0, ATOM_RADIUS - 6.0) * 2
|
|
181
|
+
path.addEllipse(QRectF(-hit_r, -hit_r, hit_r * 2.0, hit_r * 2.0))
|
|
182
|
+
return path
|
|
183
|
+
|
|
184
|
+
view = scene.views()[0]
|
|
185
|
+
scale = view.transform().m11()
|
|
186
|
+
|
|
187
|
+
scene_radius = DESIRED_ATOM_PIXEL_RADIUS / scale
|
|
188
|
+
|
|
189
|
+
path = QPainterPath()
|
|
190
|
+
path.addEllipse(QPointF(0, 0), scene_radius, scene_radius)
|
|
191
|
+
return path
|
|
192
|
+
|
|
193
|
+
def paint(self, painter, option, widget):
|
|
194
|
+
# Color logic: check if we should use bond color (uniform) or CPK (element-specific)
|
|
195
|
+
color = CPK_COLORS.get(self.symbol, CPK_COLORS['DEFAULT'])
|
|
196
|
+
try:
|
|
197
|
+
if self.scene() and self.scene().views():
|
|
198
|
+
win = self.scene().views()[0].window()
|
|
199
|
+
if win and hasattr(win, 'settings'):
|
|
200
|
+
if win.settings.get('atom_use_bond_color_2d', False):
|
|
201
|
+
bond_col = win.settings.get('bond_color_2d', '#222222')
|
|
202
|
+
color = QColor(bond_col)
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
if self.is_visible:
|
|
207
|
+
# 1. 描画の準備
|
|
208
|
+
# Ensure correct font is used (self.font should be updated by update_style)
|
|
209
|
+
painter.setFont(self.font)
|
|
210
|
+
fm = painter.fontMetrics()
|
|
211
|
+
|
|
212
|
+
# --- 水素部分のテキストを作成 ---
|
|
213
|
+
hydrogen_part = ""
|
|
214
|
+
if self.implicit_h_count > 0:
|
|
215
|
+
is_skeletal_carbon = (self.symbol == 'C' and
|
|
216
|
+
self.charge == 0 and
|
|
217
|
+
self.radical == 0 and
|
|
218
|
+
len(self.bonds) > 0)
|
|
219
|
+
if not is_skeletal_carbon:
|
|
220
|
+
hydrogen_part = "H"
|
|
221
|
+
if self.implicit_h_count > 1:
|
|
222
|
+
subscript_map = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
|
|
223
|
+
hydrogen_part += str(self.implicit_h_count).translate(subscript_map)
|
|
224
|
+
|
|
225
|
+
# --- テキストを反転させるか決定 ---
|
|
226
|
+
flip_text = False
|
|
227
|
+
# 水素ラベルがあり、結合が1本以上ある場合のみ反転を考慮
|
|
228
|
+
if hydrogen_part and self.bonds:
|
|
229
|
+
|
|
230
|
+
# 相対的なX座標で、結合が左右どちらに偏っているか判定
|
|
231
|
+
my_pos_x = self.pos().x()
|
|
232
|
+
total_dx = 0.0
|
|
233
|
+
# Defensive: some bonds may have missing atom references (None) or
|
|
234
|
+
# wrappers that were deleted by SIP. Only accumulate valid partner positions.
|
|
235
|
+
for bond in self.bonds:
|
|
236
|
+
try:
|
|
237
|
+
other_atom = bond.atom1 if bond.atom2 is self else bond.atom2
|
|
238
|
+
if other_atom is None:
|
|
239
|
+
continue
|
|
240
|
+
# If SIP reports the wrapper as deleted, skip it
|
|
241
|
+
try:
|
|
242
|
+
if sip_isdeleted_safe(other_atom):
|
|
243
|
+
continue
|
|
244
|
+
except Exception:
|
|
245
|
+
# If sip check fails, continue defensively
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
other_pos = None
|
|
249
|
+
try:
|
|
250
|
+
other_pos = other_atom.pos()
|
|
251
|
+
except Exception:
|
|
252
|
+
# Accessing .pos() may raise if the C++ object was destroyed
|
|
253
|
+
other_pos = None
|
|
254
|
+
|
|
255
|
+
if other_pos is None:
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
total_dx += (other_pos.x() - my_pos_x)
|
|
259
|
+
except Exception:
|
|
260
|
+
# Skip any problematic bond/partner rather than crashing the paint
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# 結合が主に右側にある場合はテキストを反転させる
|
|
264
|
+
if total_dx > 0:
|
|
265
|
+
flip_text = True
|
|
266
|
+
|
|
267
|
+
# --- 表示テキストとアライメントを最終決定 ---
|
|
268
|
+
if flip_text:
|
|
269
|
+
display_text = hydrogen_part + self.symbol
|
|
270
|
+
alignment_flag = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
|
271
|
+
else:
|
|
272
|
+
display_text = self.symbol + hydrogen_part
|
|
273
|
+
alignment_flag = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
|
274
|
+
|
|
275
|
+
text_rect = fm.boundingRect(display_text)
|
|
276
|
+
text_rect.adjust(-2, -2, 2, 2)
|
|
277
|
+
symbol_rect = fm.boundingRect(self.symbol) # 主元素のみの幅を計算
|
|
278
|
+
|
|
279
|
+
# --- テキストの描画位置を決定 ---
|
|
280
|
+
# 水素ラベルがない場合 (従来通り中央揃え)
|
|
281
|
+
if not hydrogen_part:
|
|
282
|
+
alignment_flag = Qt.AlignmentFlag.AlignCenter
|
|
283
|
+
text_rect.moveCenter(QPointF(0, 0).toPoint())
|
|
284
|
+
# 水素ラベルがあり、反転する場合 (右揃え)
|
|
285
|
+
elif flip_text:
|
|
286
|
+
# 主元素の中心が原子の中心に来るように、矩形の右端を調整
|
|
287
|
+
offset_x = symbol_rect.width() // 2
|
|
288
|
+
text_rect.moveTo(offset_x - text_rect.width(), -text_rect.height() // 2)
|
|
289
|
+
# 水素ラベルがあり、反転しない場合 (左揃え)
|
|
290
|
+
else:
|
|
291
|
+
# 主元素の中心が原子の中心に来るように、矩形の左端を調整
|
|
292
|
+
offset_x = -symbol_rect.width() // 2
|
|
293
|
+
text_rect.moveTo(offset_x, -text_rect.height() // 2)
|
|
294
|
+
|
|
295
|
+
# 2. 原子記号の背景を処理(白で塗りつぶす か 透明なら切り抜く)
|
|
296
|
+
if self.scene():
|
|
297
|
+
bg_brush = self.scene().backgroundBrush()
|
|
298
|
+
bg_rect = text_rect.adjusted(-5, -8, 5, 8)
|
|
299
|
+
|
|
300
|
+
if bg_brush.style() == Qt.BrushStyle.NoBrush:
|
|
301
|
+
# 背景が透明の場合は、CompositionMode_Clearを使って
|
|
302
|
+
# 重なっている結合の線を「消しゴム」のように消す
|
|
303
|
+
painter.save()
|
|
304
|
+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear)
|
|
305
|
+
painter.setBrush(QColor(0, 0, 0, 255)) # 色は何でも良い(アルファが重要)
|
|
306
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
307
|
+
painter.drawEllipse(bg_rect)
|
|
308
|
+
painter.restore()
|
|
309
|
+
else:
|
|
310
|
+
# 背景がある場合は、その背景色で塗りつぶす(従来通り)
|
|
311
|
+
painter.setBrush(bg_brush)
|
|
312
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
313
|
+
painter.drawEllipse(bg_rect)
|
|
314
|
+
|
|
315
|
+
# 3. 原子記号自体を描画
|
|
316
|
+
# Color is already determined above
|
|
317
|
+
painter.setPen(QPen(color))
|
|
318
|
+
painter.drawText(text_rect, int(alignment_flag), display_text)
|
|
319
|
+
|
|
320
|
+
# --- 電荷とラジカルの描画 ---
|
|
321
|
+
if self.charge != 0:
|
|
322
|
+
# Chemical convention: single charge as "+"/"-", multiple as "2+"/"2-"
|
|
323
|
+
if self.charge == 1:
|
|
324
|
+
charge_str = "+"
|
|
325
|
+
elif self.charge == -1:
|
|
326
|
+
charge_str = "-"
|
|
327
|
+
else:
|
|
328
|
+
sign = '+' if self.charge > 0 else '-'
|
|
329
|
+
charge_str = f"{abs(self.charge)}{sign}"
|
|
330
|
+
charge_font = QFont("Arial", 12, QFont.Weight.Bold)
|
|
331
|
+
painter.setFont(charge_font)
|
|
332
|
+
charge_rect = painter.fontMetrics().boundingRect(charge_str)
|
|
333
|
+
# 電荷の位置も反転に対応
|
|
334
|
+
if flip_text:
|
|
335
|
+
charge_pos = QPointF(text_rect.left() - charge_rect.width() -2, text_rect.top() + charge_rect.height() - 2)
|
|
336
|
+
else:
|
|
337
|
+
charge_pos = QPointF(text_rect.right() + 2, text_rect.top() + charge_rect.height() - 2)
|
|
338
|
+
painter.setPen(Qt.GlobalColor.black)
|
|
339
|
+
painter.drawText(charge_pos, charge_str)
|
|
340
|
+
|
|
341
|
+
if self.radical > 0:
|
|
342
|
+
painter.setBrush(QBrush(Qt.GlobalColor.black))
|
|
343
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
344
|
+
radical_pos_y = text_rect.top() - 5
|
|
345
|
+
if self.radical == 1:
|
|
346
|
+
painter.drawEllipse(QPointF(text_rect.center().x(), radical_pos_y), 3, 3)
|
|
347
|
+
elif self.radical == 2:
|
|
348
|
+
painter.drawEllipse(QPointF(text_rect.center().x() - 5, radical_pos_y), 3, 3)
|
|
349
|
+
painter.drawEllipse(QPointF(text_rect.center().x() + 5, radical_pos_y), 3, 3)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# --- 選択時のハイライトなど ---
|
|
353
|
+
if self.has_problem:
|
|
354
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
355
|
+
painter.setPen(QPen(QColor(255, 0, 0, 200), 4))
|
|
356
|
+
painter.drawRect(self.boundingRect())
|
|
357
|
+
elif self.isSelected():
|
|
358
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
359
|
+
painter.setPen(QPen(QColor(0, 100, 255), 3))
|
|
360
|
+
painter.drawRect(self.boundingRect())
|
|
361
|
+
if (not self.isSelected()) and getattr(self, 'hovered', False):
|
|
362
|
+
pen = QPen(QColor(144, 238, 144, 200), 3)
|
|
363
|
+
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
|
364
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
365
|
+
painter.setPen(pen)
|
|
366
|
+
painter.drawRect(self.boundingRect())
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# 約203行目 AtomItem クラス内
|
|
372
|
+
|
|
373
|
+
def itemChange(self, change, value):
|
|
374
|
+
res = super().itemChange(change, value)
|
|
375
|
+
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
|
376
|
+
if self.flags() & QGraphicsItem.GraphicsItemFlag.ItemIsMovable:
|
|
377
|
+
# Prevent cascading updates during batch operations
|
|
378
|
+
if not getattr(self, '_updating_position', False):
|
|
379
|
+
for bond in self.bonds:
|
|
380
|
+
if bond.scene(): # Only update if bond is still in scene
|
|
381
|
+
bond.update_position()
|
|
382
|
+
|
|
383
|
+
return res
|
|
384
|
+
|
|
385
|
+
def hoverEnterEvent(self, event):
|
|
386
|
+
# シーンのモードにかかわらず、ホバー時にハイライトを有効にする
|
|
387
|
+
self.hovered = True
|
|
388
|
+
self.update()
|
|
389
|
+
super().hoverEnterEvent(event)
|
|
390
|
+
|
|
391
|
+
def hoverLeaveEvent(self, event):
|
|
392
|
+
if self.hovered:
|
|
393
|
+
self.hovered = False
|
|
394
|
+
self.update()
|
|
395
|
+
super().hoverLeaveEvent(event)
|