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.
Files changed (59) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +292 -0
  7. moleditpy_linux/modules/alignment_dialog.py +272 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/file_icon.ico +0 -0
  11. moleditpy_linux/modules/assets/icon.icns +0 -0
  12. moleditpy_linux/modules/assets/icon.ico +0 -0
  13. moleditpy_linux/modules/assets/icon.png +0 -0
  14. moleditpy_linux/modules/atom_item.py +395 -0
  15. moleditpy_linux/modules/bond_item.py +464 -0
  16. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  17. moleditpy_linux/modules/calculation_worker.py +766 -0
  18. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  19. moleditpy_linux/modules/constants.py +88 -0
  20. moleditpy_linux/modules/constrained_optimization_dialog.py +678 -0
  21. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  22. moleditpy_linux/modules/custom_qt_interactor.py +102 -0
  23. moleditpy_linux/modules/dialog3_d_picking_mixin.py +141 -0
  24. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  25. moleditpy_linux/modules/main_window.py +850 -0
  26. moleditpy_linux/modules/main_window_app_state.py +787 -0
  27. moleditpy_linux/modules/main_window_compute.py +1242 -0
  28. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  29. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  30. moleditpy_linux/modules/main_window_edit_actions.py +1565 -0
  31. moleditpy_linux/modules/main_window_export.py +917 -0
  32. moleditpy_linux/modules/main_window_main_init.py +2100 -0
  33. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  34. moleditpy_linux/modules/main_window_project_io.py +434 -0
  35. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  36. moleditpy_linux/modules/main_window_ui_manager.py +602 -0
  37. moleditpy_linux/modules/main_window_view_3d.py +1539 -0
  38. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  39. moleditpy_linux/modules/mirror_dialog.py +122 -0
  40. moleditpy_linux/modules/molecular_data.py +302 -0
  41. moleditpy_linux/modules/molecule_scene.py +2000 -0
  42. moleditpy_linux/modules/move_group_dialog.py +600 -0
  43. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  44. moleditpy_linux/modules/planarize_dialog.py +220 -0
  45. moleditpy_linux/modules/plugin_interface.py +215 -0
  46. moleditpy_linux/modules/plugin_manager.py +473 -0
  47. moleditpy_linux/modules/plugin_manager_window.py +274 -0
  48. moleditpy_linux/modules/settings_dialog.py +1503 -0
  49. moleditpy_linux/modules/template_preview_item.py +157 -0
  50. moleditpy_linux/modules/template_preview_view.py +74 -0
  51. moleditpy_linux/modules/translation_dialog.py +364 -0
  52. moleditpy_linux/modules/user_template_dialog.py +692 -0
  53. moleditpy_linux/modules/zoomable_view.py +129 -0
  54. moleditpy_linux-2.4.1.dist-info/METADATA +954 -0
  55. moleditpy_linux-2.4.1.dist-info/RECORD +59 -0
  56. moleditpy_linux-2.4.1.dist-info/WHEEL +5 -0
  57. moleditpy_linux-2.4.1.dist-info/entry_points.txt +2 -0
  58. moleditpy_linux-2.4.1.dist-info/licenses/LICENSE +674 -0
  59. moleditpy_linux-2.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,464 @@
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, QGraphicsScene
14
+
15
+ from PyQt6.QtGui import (
16
+ QPen, QBrush, QColor, QFont, QPolygonF,
17
+ QPainterPath, QPainterPathStroker, QFontMetricsF
18
+ )
19
+
20
+ from PyQt6.QtCore import (
21
+ Qt, QPointF, QRectF, QLineF
22
+ )
23
+
24
+ try:
25
+ from .constants import (
26
+ EZ_LABEL_BOX_SIZE, EZ_LABEL_TEXT_OUTLINE, EZ_LABEL_MARGIN,
27
+ BOND_OFFSET, FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
28
+ HOVER_PEN_WIDTH, DESIRED_BOND_PIXEL_WIDTH,
29
+ )
30
+ except Exception:
31
+ from modules.constants import (
32
+ EZ_LABEL_BOX_SIZE, EZ_LABEL_TEXT_OUTLINE, EZ_LABEL_MARGIN,
33
+ BOND_OFFSET, FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD,
34
+ HOVER_PEN_WIDTH, DESIRED_BOND_PIXEL_WIDTH,
35
+ )
36
+
37
+ class BondItem(QGraphicsItem):
38
+
39
+ def get_ez_label_rect(self):
40
+ """E/Zラベルの描画範囲(シーン座標)を返す。ラベルが無い場合はNone。"""
41
+ if self.order != 2 or self.stereo not in [3, 4]:
42
+ return None
43
+ line = self.get_line_in_local_coords()
44
+ center = line.center()
45
+ label_width = EZ_LABEL_BOX_SIZE
46
+ label_height = EZ_LABEL_BOX_SIZE
47
+ label_rect = QRectF(center.x() - label_width/2, center.y() - label_height/2, label_width, label_height)
48
+ # シーン座標に変換
49
+ return self.mapToScene(label_rect).boundingRect()
50
+ def set_stereo(self, new_stereo):
51
+ try:
52
+ # ラベルを消す場合は、消す前のboundingRectをscene().invalidateで強制的に無効化
53
+ if new_stereo == 0 and self.stereo in [3, 4] and self.scene():
54
+ rect = self.mapToScene(self.boundingRect()).boundingRect()
55
+ self.scene().invalidate(rect, QGraphicsScene.SceneLayer.BackgroundLayer | QGraphicsScene.SceneLayer.ForegroundLayer)
56
+
57
+ self.prepareGeometryChange()
58
+ self.stereo = new_stereo
59
+ self.update()
60
+
61
+ if self.scene() and self.scene().views():
62
+ try:
63
+ self.scene().views()[0].viewport().update()
64
+ except (IndexError, RuntimeError):
65
+ # Handle case where views are being destroyed
66
+ pass
67
+
68
+ except Exception as e:
69
+ print(f"Error in BondItem.set_stereo: {e}")
70
+ # Continue without crashing
71
+ self.stereo = new_stereo
72
+
73
+ def set_order(self, new_order):
74
+ self.prepareGeometryChange()
75
+ self.order = new_order
76
+ self.update()
77
+ if self.scene() and self.scene().views():
78
+ self.scene().views()[0].viewport().update()
79
+ def __init__(self, atom1_item, atom2_item, order=1, stereo=0):
80
+ super().__init__()
81
+ # Validate input parameters
82
+ if atom1_item is None or atom2_item is None:
83
+ raise ValueError("BondItem requires non-None atom items")
84
+ self.atom1, self.atom2, self.order, self.stereo = atom1_item, atom2_item, order, stereo
85
+ self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
86
+ self.pen = QPen(Qt.GlobalColor.black, 2)
87
+ self.setZValue(0)
88
+ self.update_position()
89
+ self.setAcceptHoverEvents(True)
90
+ self.hovered = False
91
+
92
+
93
+ def get_line_in_local_coords(self):
94
+ if self.atom1 is None or self.atom2 is None:
95
+ return QLineF(0, 0, 0, 0)
96
+ try:
97
+ p2 = self.mapFromItem(self.atom2, 0, 0)
98
+ return QLineF(QPointF(0, 0), p2)
99
+ except (RuntimeError, TypeError):
100
+ # Handle case where atoms are deleted from scene
101
+ return QLineF(0, 0, 0, 0)
102
+
103
+ def boundingRect(self):
104
+ try:
105
+ line = self.get_line_in_local_coords()
106
+ except Exception:
107
+ line = QLineF(0, 0, 0, 0)
108
+
109
+ # Get dynamic bond offset (spacing)
110
+ bond_offset = 3.5
111
+ try:
112
+ if self.scene() and hasattr(self.scene(), 'views') and self.scene().views():
113
+ win = self.scene().views()[0].window()
114
+ if win and hasattr(win, 'settings'):
115
+ # Use specific spacing based on bond order
116
+ if getattr(self, 'order', 1) == 3:
117
+ bond_offset = win.settings.get('bond_spacing_triple_2d', 3.5)
118
+ else:
119
+ bond_offset = win.settings.get('bond_spacing_double_2d', 3.5)
120
+ except Exception:
121
+ bond_offset = globals().get('BOND_OFFSET', 3.5)
122
+
123
+ extra = (getattr(self, 'order', 1) - 1) * bond_offset + 20
124
+ rect = QRectF(line.p1(), line.p2()).normalized().adjusted(-extra, -extra, extra, extra)
125
+
126
+ # E/Zラベルの描画範囲も考慮して拡張(QFontMetricsFで正確に)
127
+ if self.order == 2 and self.stereo in [3, 4]:
128
+ font = QFont(FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD)
129
+ font.setItalic(True)
130
+ text = "Z" if self.stereo == 3 else "E"
131
+ fm = QFontMetricsF(font)
132
+ text_rect = fm.boundingRect(text)
133
+ outline = EZ_LABEL_TEXT_OUTLINE # 輪郭の太さ分
134
+ margin = EZ_LABEL_MARGIN # 追加余白
135
+ center = line.center()
136
+ label_rect = QRectF(center.x() - text_rect.width()/2 - outline - margin,
137
+ center.y() - text_rect.height()/2 - outline - margin,
138
+ text_rect.width() + 2*outline + 2*margin,
139
+ text_rect.height() + 2*outline + 2*margin)
140
+ rect = rect.united(label_rect)
141
+ return rect
142
+
143
+ def shape(self):
144
+ path = QPainterPath()
145
+ try:
146
+ line = self.get_line_in_local_coords()
147
+ except Exception:
148
+ return path
149
+ if line.length() == 0:
150
+ return path
151
+
152
+ scene = self.scene()
153
+ if not scene or not scene.views():
154
+ return super().shape()
155
+
156
+ view = scene.views()[0]
157
+ scale = view.transform().m11()
158
+
159
+ # Dynamic bond width
160
+ width_2d = 2.0
161
+ try:
162
+ if view.window() and hasattr(view.window(), 'settings'):
163
+ width_2d = view.window().settings.get('bond_width_2d', 2.0)
164
+ except Exception:
165
+ pass
166
+
167
+ # Hit area should be roughly closely matched or slightly larger than visual
168
+ # Ensure minimum hit width for usability
169
+ scene_width = max(DESIRED_BOND_PIXEL_WIDTH, width_2d * 10) / scale
170
+
171
+
172
+ stroker = QPainterPathStroker()
173
+ stroker.setWidth(scene_width)
174
+ stroker.setCapStyle(Qt.PenCapStyle.RoundCap)
175
+ stroker.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
176
+
177
+ center_line_path = QPainterPath(line.p1())
178
+ center_line_path.lineTo(line.p2())
179
+
180
+ return stroker.createStroke(center_line_path)
181
+
182
+ def paint(self, painter, option, widget):
183
+ if self.atom1 is None or self.atom2 is None:
184
+ return
185
+ line = self.get_line_in_local_coords()
186
+ if line.length() == 0: return
187
+
188
+ # Allow bond color override from app settings (2D color)
189
+ width_2d = 2.0
190
+
191
+ try:
192
+ sc = self.scene()
193
+ if sc is not None and hasattr(sc, 'window') and sc.window is not None:
194
+ # Get settings
195
+ settings = sc.window.settings
196
+
197
+ # Width
198
+ width_2d = settings.get('bond_width_2d', 2.0)
199
+
200
+ # Cap Style logic
201
+ cap_style_str = settings.get('bond_cap_style_2d', 'Round')
202
+ cap_style = Qt.PenCapStyle.RoundCap # Default
203
+
204
+ if cap_style_str == 'Flat':
205
+ cap_style = Qt.PenCapStyle.FlatCap
206
+ elif cap_style_str == 'Square':
207
+ cap_style = Qt.PenCapStyle.SquareCap
208
+
209
+ # Color
210
+ if self.isSelected():
211
+ bond_color = QColor("blue") # Selection color
212
+ else:
213
+ bond_hex = settings.get('bond_color_2d', '#222222')
214
+ bond_color = QColor(bond_hex)
215
+
216
+ pen = QPen(bond_color, width_2d)
217
+ pen.setCapStyle(cap_style)
218
+ painter.setPen(pen)
219
+
220
+ else:
221
+ painter.setPen(self.pen)
222
+ except Exception:
223
+ painter.setPen(self.pen)
224
+
225
+ painter.setBrush(QBrush(Qt.GlobalColor.black))
226
+
227
+ # --- 立体化学 (Wedge/Dash) の描画 ---
228
+ if self.order == 1 and self.stereo in [1, 2]:
229
+ vec = line.unitVector()
230
+ normal = vec.normalVector()
231
+ p1 = line.p1() + vec.p2() * 5
232
+ p2 = line.p2() - vec.p2() * 5
233
+
234
+ if self.stereo == 1: # Wedge (くさび形)
235
+ offset = QPointF(normal.dx(), normal.dy()) * 6.0
236
+ poly = QPolygonF([p1, p2 + offset, p2 - offset])
237
+ painter.drawPolygon(poly)
238
+
239
+ elif self.stereo == 2: # Dash (破線)
240
+ painter.save()
241
+ if not self.isSelected():
242
+ pen = painter.pen()
243
+ pen.setWidthF(2.5)
244
+ painter.setPen(pen)
245
+
246
+ num_dashes = 8
247
+ for i in range(num_dashes + 1):
248
+ t = i / num_dashes
249
+ start_pt = p1 * (1 - t) + p2 * t
250
+ width = 12.0 * t
251
+ offset = QPointF(normal.dx(), normal.dy()) * width / 2.0
252
+ painter.drawLine(start_pt - offset, start_pt + offset)
253
+ painter.restore()
254
+
255
+ # --- 通常の結合 (単/二重/三重) の描画 ---
256
+ else:
257
+ if self.order == 1:
258
+ painter.drawLine(line)
259
+ else:
260
+ v = line.unitVector().normalVector()
261
+ # Use dynamic offset
262
+ bond_offset = 3.5
263
+ try:
264
+ sc = self.scene()
265
+ if sc and sc.views() and hasattr(sc.views()[0].window(), 'settings'):
266
+ if self.order == 3:
267
+ bond_offset = sc.views()[0].window().settings.get('bond_spacing_triple_2d', 3.5)
268
+ else:
269
+ bond_offset = sc.views()[0].window().settings.get('bond_spacing_double_2d', 3.5)
270
+ except Exception:
271
+ bond_offset = globals().get('BOND_OFFSET', 3.5)
272
+
273
+ offset = QPointF(v.dx(), v.dy()) * bond_offset
274
+
275
+ if self.order == 2:
276
+ # 環構造かどうかを判定し、描画方法を変更
277
+ is_in_ring = False
278
+ ring_center = None
279
+
280
+ try:
281
+ # シーンからRDKit分子を取得
282
+ sc = self.scene()
283
+ if sc and hasattr(sc, 'window') and sc.window:
284
+ # 2DデータからRDKit分子を生成
285
+ mol = sc.window.data.to_rdkit_mol(use_2d_stereo=False)
286
+ if mol:
287
+ # この結合に対応するRDKitボンドを探す
288
+ atom1_id = self.atom1.atom_id
289
+ atom2_id = self.atom2.atom_id
290
+
291
+ # RDKitインデックスを取得
292
+ rdkit_idx1 = None
293
+ rdkit_idx2 = None
294
+ for atom in mol.GetAtoms():
295
+ if atom.HasProp("_original_atom_id"):
296
+ orig_id = atom.GetIntProp("_original_atom_id")
297
+ if orig_id == atom1_id:
298
+ rdkit_idx1 = atom.GetIdx()
299
+ elif orig_id == atom2_id:
300
+ rdkit_idx2 = atom.GetIdx()
301
+
302
+ if rdkit_idx1 is not None and rdkit_idx2 is not None:
303
+ bond = mol.GetBondBetweenAtoms(rdkit_idx1, rdkit_idx2)
304
+ if bond and bond.IsInRing():
305
+ is_in_ring = True
306
+ # 環の中心を計算(この結合を含む最小環)
307
+ from rdkit import Chem
308
+ ring_info = mol.GetRingInfo()
309
+ for ring in ring_info.AtomRings():
310
+ if rdkit_idx1 in ring and rdkit_idx2 in ring:
311
+ # 環の原子位置の平均を計算
312
+ ring_positions = []
313
+ for atom_idx in ring:
314
+ # 対応するエディタ側の原子を探す
315
+ rdkit_atom = mol.GetAtomWithIdx(atom_idx)
316
+ if rdkit_atom.HasProp("_original_atom_id"):
317
+ editor_atom_id = rdkit_atom.GetIntProp("_original_atom_id")
318
+ if editor_atom_id in sc.window.data.atoms:
319
+ atom_item = sc.window.data.atoms[editor_atom_id]['item']
320
+ if atom_item:
321
+ ring_positions.append(atom_item.pos())
322
+
323
+ if ring_positions:
324
+ # 環の中心を計算
325
+ center_x = sum(p.x() for p in ring_positions) / len(ring_positions)
326
+ center_y = sum(p.y() for p in ring_positions) / len(ring_positions)
327
+ ring_center = QPointF(center_x, center_y)
328
+ break
329
+ except Exception as e:
330
+ # エラーが発生した場合は通常の描画にフォールバック
331
+ is_in_ring = False
332
+
333
+ v = line.unitVector().normalVector()
334
+ # Re-calculate offset in case loop variable scope issue, though strictly not needed if offset defined above works
335
+ offset = QPointF(v.dx(), v.dy()) * bond_offset
336
+
337
+ if is_in_ring and ring_center:
338
+ # 環構造: 1本の中心線(単結合位置) + 1本の短い内側線
339
+ # 結合の中心から環の中心への方向を計算
340
+ bond_center = line.center()
341
+
342
+ # ローカル座標系での環中心方向
343
+ local_ring_center = self.mapFromScene(ring_center)
344
+ local_bond_center = line.center()
345
+ inward_vec = local_ring_center - local_bond_center
346
+
347
+ # offsetとinward_vecの内積で内側を判定
348
+ if QPointF.dotProduct(offset, inward_vec) > 0:
349
+ # offsetが内側方向(2倍のオフセット)
350
+ inner_offset = offset * 2
351
+ else:
352
+ # -offsetが内側方向(2倍のオフセット)
353
+ inner_offset = -offset * 2
354
+
355
+ # 中心線を描画(単結合と同じ位置)
356
+ painter.drawLine(line)
357
+
358
+ # 内側の短い線を描画(80%の長さ)
359
+ inner_line = line.translated(inner_offset)
360
+ shorten_factor = 0.8
361
+ p1 = inner_line.p1()
362
+ p2 = inner_line.p2()
363
+ center = QPointF((p1.x() + p2.x()) / 2, (p1.y() + p2.y()) / 2)
364
+ shortened_p1 = center + (p1 - center) * shorten_factor
365
+ shortened_p2 = center + (p2 - center) * shorten_factor
366
+ painter.drawLine(QLineF(shortened_p1, shortened_p2))
367
+ else:
368
+ # 非環構造: 従来の2本の平行線
369
+ line1 = line.translated(offset)
370
+ line2 = line.translated(-offset)
371
+ painter.drawLine(line1)
372
+ painter.drawLine(line2)
373
+
374
+ # E/Z ラベルの描画処理
375
+ if self.stereo in [3, 4]:
376
+ painter.save() # 現在の描画設定を保存
377
+
378
+ # --- ラベルの設定 ---
379
+ font = QFont(FONT_FAMILY, FONT_SIZE_LARGE, FONT_WEIGHT_BOLD)
380
+ font.setItalic(True)
381
+ text_color = QColor("gray")
382
+ # 輪郭の色を背景色と同じにする(scene()がNoneのときは安全なフォールバックを使う)
383
+ outline_color = None
384
+ try:
385
+ sc = self.scene()
386
+ if sc is not None:
387
+ outline_color = sc.backgroundBrush().color()
388
+ except Exception:
389
+ outline_color = None
390
+ if outline_color is None:
391
+ # デフォルトでは白背景を想定して黒系の輪郭が見やすい
392
+ outline_color = QColor(255, 255, 255)
393
+
394
+ # --- 描画パスの作成 ---
395
+ text = "Z" if self.stereo == 3 else "E"
396
+ path = QPainterPath()
397
+
398
+ # テキストが正確に中央に来るように位置を計算
399
+ fm = QFontMetricsF(font)
400
+ text_rect = fm.boundingRect(text)
401
+ text_rect.moveCenter(line.center())
402
+ path.addText(text_rect.topLeft(), font, text)
403
+
404
+ # --- 輪郭の描画 ---
405
+ stroker = QPainterPathStroker()
406
+ stroker.setWidth(EZ_LABEL_TEXT_OUTLINE) # 輪郭の太さ
407
+ outline_path = stroker.createStroke(path)
408
+
409
+ painter.setBrush(outline_color)
410
+ painter.setPen(Qt.PenStyle.NoPen)
411
+ painter.drawPath(outline_path)
412
+
413
+ # --- 文字本体の描画 ---
414
+ painter.setBrush(text_color)
415
+ painter.setPen(text_color)
416
+ painter.drawPath(path)
417
+
418
+ painter.restore() # 描画設定を元に戻す
419
+
420
+ elif self.order == 3:
421
+ painter.drawLine(line)
422
+ painter.drawLine(line.translated(offset))
423
+ painter.drawLine(line.translated(-offset))
424
+
425
+ # --- 2. ホバー時のエフェクトを上から重ねて描画 ---
426
+ if (not self.isSelected()) and getattr(self, 'hovered', False):
427
+ try:
428
+ # ホバー時のハイライトを太めの半透明な線で描画
429
+ hover_pen = QPen(QColor(144, 238, 144, 180), HOVER_PEN_WIDTH) # LightGreen, 半透明
430
+ hover_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
431
+ painter.setPen(hover_pen)
432
+ painter.drawLine(line)
433
+ except Exception:
434
+ pass
435
+
436
+
437
+
438
+ def update_position(self):
439
+ try:
440
+ self.prepareGeometryChange()
441
+ if self.atom1:
442
+ self.setPos(self.atom1.pos())
443
+ self.update()
444
+ except Exception as e:
445
+ print(f"Error updating bond position: {e}")
446
+ # Continue without crashing
447
+
448
+
449
+ def hoverEnterEvent(self, event):
450
+ scene = self.scene()
451
+ mode = getattr(scene, 'mode', '')
452
+ self.hovered = True
453
+ self.update()
454
+ if self.scene():
455
+ self.scene().set_hovered_item(self)
456
+ super().hoverEnterEvent(event)
457
+
458
+ def hoverLeaveEvent(self, event):
459
+ if self.hovered:
460
+ self.hovered = False
461
+ self.update()
462
+ if self.scene():
463
+ self.scene().set_hovered_item(None)
464
+ super().hoverLeaveEvent(event)