nodeeditor-qtpy 0.8.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 (40) hide show
  1. nodeeditor/__init__.py +93 -0
  2. nodeeditor/node_colors_config.py +343 -0
  3. nodeeditor/node_content_widget.py +129 -0
  4. nodeeditor/node_decorators.py +65 -0
  5. nodeeditor/node_edge.py +518 -0
  6. nodeeditor/node_edge_dragging.py +185 -0
  7. nodeeditor/node_edge_intersect.py +219 -0
  8. nodeeditor/node_edge_rerouting.py +216 -0
  9. nodeeditor/node_edge_snapping.py +95 -0
  10. nodeeditor/node_edge_validators.py +82 -0
  11. nodeeditor/node_editor_widget.py +257 -0
  12. nodeeditor/node_editor_window.py +448 -0
  13. nodeeditor/node_graph.py +583 -0
  14. nodeeditor/node_graphics_cutline.py +57 -0
  15. nodeeditor/node_graphics_edge.py +375 -0
  16. nodeeditor/node_graphics_edge_path.py +226 -0
  17. nodeeditor/node_graphics_node.py +693 -0
  18. nodeeditor/node_graphics_scene.py +369 -0
  19. nodeeditor/node_graphics_socket.py +262 -0
  20. nodeeditor/node_graphics_view.py +907 -0
  21. nodeeditor/node_group.py +1286 -0
  22. nodeeditor/node_group_node.py +5 -0
  23. nodeeditor/node_icon_content_widget.py +176 -0
  24. nodeeditor/node_icon_graphics_node.py +524 -0
  25. nodeeditor/node_multi_input_node.py +450 -0
  26. nodeeditor/node_node.py +848 -0
  27. nodeeditor/node_scene.py +1106 -0
  28. nodeeditor/node_scene_clipboard.py +314 -0
  29. nodeeditor/node_scene_history.py +556 -0
  30. nodeeditor/node_serializable.py +43 -0
  31. nodeeditor/node_socket.py +445 -0
  32. nodeeditor/node_socket_shapes.py +261 -0
  33. nodeeditor/py.typed +0 -0
  34. nodeeditor/qss/nodestyle.qss +3 -0
  35. nodeeditor/utils.py +108 -0
  36. nodeeditor/utils_no_qt.py +21 -0
  37. nodeeditor_qtpy-0.8.1.dist-info/METADATA +165 -0
  38. nodeeditor_qtpy-0.8.1.dist-info/RECORD +40 -0
  39. nodeeditor_qtpy-0.8.1.dist-info/WHEEL +4 -0
  40. nodeeditor_qtpy-0.8.1.dist-info/licenses/LICENCE +23 -0
@@ -0,0 +1,524 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ A module containing Graphics representation of :class:`~nodeeditor.node_node.Node`
4
+ """
5
+ from qtpy.QtWidgets import QGraphicsItem, QWidget, QGraphicsTextItem, QGraphicsPixmapItem, QGraphicsSceneHoverEvent
6
+ from qtpy.QtGui import QFont, QColor, QPen, QBrush, QPainterPath, QPixmap
7
+ from qtpy.QtCore import Qt, QRectF
8
+
9
+ from typing import TYPE_CHECKING, List, Mapping, Optional, Tuple, Any
10
+
11
+ from nodeeditor.node_colors_config import get_color_scheme
12
+
13
+
14
+ from nodeeditor.node_graphics_node import QDMGraphicsNodeLabel
15
+
16
+ if TYPE_CHECKING:
17
+ from nodeeditor.node_graphics_view import QDMGraphicsView
18
+ from nodeeditor.node_edge import Edge
19
+ from nodeeditor.node_socket import Socket
20
+ from nodeeditor.node_node import Node
21
+
22
+
23
+ class QDMIconGraphicsNode(QGraphicsItem):
24
+ """Class describing Graphics representation of :class:`~nodeeditor.node_node.Node`"""
25
+
26
+ def __init__(self, node: 'Node', parent: QGraphicsItem = None, icon_path: QPixmap = None) -> None:
27
+ """
28
+ :param node: reference to :class:`~nodeeditor.node_node.Node`
29
+ :type node: :class:`~nodeeditor.node_node.Node`
30
+ :param parent: parent widget
31
+ :type parent: QWidget
32
+
33
+ :Instance Attributes:
34
+
35
+ - **node** - reference to :class:`~nodeeditor.node_node.Node`
36
+ """
37
+ super().__init__(parent)
38
+ self.node = node
39
+ self.icon_path = icon_path
40
+
41
+ # init our flags
42
+ self.hovered = False
43
+ self._was_moved = False
44
+ self._last_selected_state = False
45
+
46
+ self.initSizes()
47
+ self.initAssets()
48
+ self.initUI()
49
+
50
+ @property
51
+ def content(self):
52
+ """Reference to `Node Content`"""
53
+ return self.node.content if self.node else None
54
+
55
+ @property
56
+ def title(self):
57
+ """title of this `Node`
58
+
59
+ :getter: current Graphics Node title
60
+ :setter: stores and make visible the new title
61
+ :type: str
62
+ """
63
+ return self._title
64
+
65
+ @title.setter
66
+ def title(self, value) -> None:
67
+ self._title = value
68
+ # self.title_item.setPlainText(self._title)
69
+
70
+ def initUI(self) -> None:
71
+ """Set up this ``QGraphicsItem``"""
72
+ self.setFlag(QGraphicsItem.ItemIsSelectable)
73
+ self.setFlag(QGraphicsItem.ItemIsMovable)
74
+ # Qt then tells us when it has moved the item, which is how the
75
+ # connected edges are refreshed without rescanning the whole scene.
76
+ self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
77
+ self.setAcceptHoverEvents(True)
78
+
79
+ # init title
80
+ self.initTitle()
81
+ # self.title = self.node.title
82
+
83
+ self.initContent()
84
+
85
+ # init icon
86
+ # self.initIcon()
87
+
88
+ self.initLabel()
89
+
90
+ def initSizes(self) -> None:
91
+ """Set up internal attributes like `width`, `height`, etc."""
92
+ self.width = 180
93
+ self.height = 240
94
+ self.edge_roundness = 10.0
95
+ self.edge_padding = 10
96
+ self.title_height = 0
97
+ self.title_horizontal_padding = 0.0
98
+ self.title_vertical_padding = 0.0
99
+ self.label_offset: float = 10.0
100
+ self._label_visible: bool = False
101
+ #: Level-of-detail state pushed by the scene (see
102
+ #: ``QDMGraphicsScene.setDetailLod``). False below the scene's
103
+ #: threshold, when labels, content widgets and sockets are skipped.
104
+ self._detail_visible: bool = True
105
+ #: Resolved per-element state pushed by the scene (see
106
+ #: ``QDMGraphicsScene.set_detail_policy``). Each flag says whether
107
+ #: that element may be drawn right now.
108
+ self._content_visible: bool = True
109
+ self._sockets_visible: bool = True
110
+ self._label_enabled: bool = True
111
+
112
+ def initAssets(self) -> None:
113
+ """Initialize ``QObjects`` like ``QColor``, ``QPen`` and ``QBrush``"""
114
+ self._title_color = Qt.white
115
+ self._title_font = QFont("Ubuntu", 10)
116
+
117
+ # Get colors from global color scheme
118
+ scheme = get_color_scheme()
119
+ self._color = QColor("#7F000000")
120
+ self._color_selected = scheme.nodes.selected
121
+ self._color_hovered = scheme.edges.hovered
122
+
123
+ self._pen_default = QPen(self._color)
124
+ self._pen_default.setWidthF(2.0)
125
+ self._pen_selected = QPen(self._color_selected)
126
+ self._pen_selected.setWidthF(2.0)
127
+ self._pen_hovered = QPen(self._color_hovered)
128
+ self._pen_hovered.setWidthF(3.0)
129
+
130
+ self._brush_title = QBrush(QColor("#FF313131"))
131
+ self._brush_background = QBrush(QColor("#E3212121"))
132
+
133
+ self._label_font = QFont("Ubuntu", 9)
134
+ self._label_color = QColor("#EEEEEE")
135
+
136
+ def onSelected(self) -> None:
137
+ """Our event handling when the node was selected"""
138
+ self.node.scene.grScene.itemSelected.emit()
139
+
140
+ def doSelect(self, new_state: bool = True) -> None:
141
+ """Safe version of selecting the `Graphics Node`. Takes care about the selection state flag used internally
142
+
143
+ :param new_state: ``True`` to select, ``False`` to deselect
144
+ :type new_state: ``bool``
145
+ """
146
+ self.setSelected(new_state)
147
+ self._last_selected_state = new_state
148
+ if new_state:
149
+ self.onSelected()
150
+
151
+ def itemChange(self, change, value):
152
+ """Refresh this node's edges when Qt moves it.
153
+
154
+ Qt moves the dragged node - and every other node in the selection -
155
+ itself. Reacting to the position change Qt already reports updates the
156
+ edges of exactly the nodes that moved, replacing the old per-mouse-move
157
+ scan of every node in the scene.
158
+ """
159
+ if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
160
+ node = getattr(self, "node", None)
161
+ if node is not None:
162
+ try:
163
+ node.updateConnectedEdges()
164
+ except (AttributeError, RuntimeError):
165
+ pass # socket list not built yet, or C++ side gone
166
+ elif change == QGraphicsItem.GraphicsItemChange.ItemSceneHasChanged:
167
+ # A node added while the view is already zoomed out must start in
168
+ # the scene's current level-of-detail state, not fully detailed:
169
+ # loading a big workflow while zoomed out would otherwise draw every
170
+ # new node's content widget until the next zoom change.
171
+ scene = value if value is not None else self.scene()
172
+ state = getattr(scene, "detail_state", None)
173
+ if callable(state):
174
+ self.setDetailState(state())
175
+ else:
176
+ lod = getattr(scene, "_detail_visible", True)
177
+ if lod != getattr(self, "_detail_visible", True):
178
+ self.setDetailVisible(lod)
179
+ elif change == QGraphicsItem.GraphicsItemChange.ItemChildAddedChange:
180
+ # Sockets and the content proxy are created after this node joined
181
+ # the scene, so the scene's one-off broadcast never reached them;
182
+ # without this a node built while zoomed out draws fully detailed
183
+ # until the next zoom change.
184
+ self._applyDetailToChild(value)
185
+ return super().itemChange(change, value)
186
+
187
+ def mouseMoveEvent(self, event) -> None:
188
+ """Overridden event to detect that we moved with this `Node`"""
189
+ super().mouseMoveEvent(event)
190
+ # Connected edges follow via itemChange(); no scene-wide scan here.
191
+ self._was_moved = True
192
+
193
+ def mouseReleaseEvent(self, event) -> None:
194
+ """Overriden event to handle when we moved, selected or deselected this `Node`"""
195
+ super().mouseReleaseEvent(event)
196
+
197
+ # handle when grNode moved
198
+ if self._was_moved:
199
+ self.node.positionChanged.emit(self.node)
200
+ print("emitting positionChanged for node:", self.node.title)
201
+ self._was_moved = False
202
+ self.node.scene.history.storeHistory(
203
+ "Node moved", setModified=True)
204
+
205
+ self.node.scene.resetLastSelectedStates()
206
+ self.doSelect() # also trigger itemSelected when node was moved
207
+
208
+ # we need to store the last selected state, because moving does also select the nodes
209
+ self.node.scene._last_selected_items = self.node.scene.getSelectedItems()
210
+
211
+ # now we want to skip storing selection
212
+ return
213
+
214
+ # handle when grNode was clicked on
215
+ if self._last_selected_state != self.isSelected() or self.node.scene._last_selected_items != self.node.scene.getSelectedItems():
216
+ self.node.scene.resetLastSelectedStates()
217
+ self._last_selected_state = self.isSelected()
218
+ self.onSelected()
219
+
220
+ def mouseDoubleClickEvent(self, event) -> None:
221
+ """Overriden event for doubleclick. Resend to `Node::onDoubleClicked`"""
222
+ self.node.onDoubleClicked(event)
223
+
224
+ def hoverEnterEvent(self, event: Optional['QGraphicsSceneHoverEvent']) -> None:
225
+ """Handle hover effect"""
226
+ self.hovered = True
227
+ self.update()
228
+
229
+ def hoverLeaveEvent(self, event: Optional['QGraphicsSceneHoverEvent']) -> None:
230
+ """Handle hover effect"""
231
+ self.hovered = False
232
+ self.update()
233
+
234
+ def boundingRect(self) -> QRectF:
235
+ """Defining Qt' bounding rectangle"""
236
+ return QRectF(
237
+ 0,
238
+ 0,
239
+ self.width,
240
+ self.height
241
+ ).normalized()
242
+
243
+ def initTitle(self) -> None:
244
+ """Set up the title Graphics representation: font, color, position, etc."""
245
+ self.title_item = QGraphicsTextItem(self)
246
+ # self.title = self.node.title
247
+ # self.title_item.node = self.node
248
+ # self.title_item.setDefaultTextColor(self._title_color)
249
+ # self.title_item.setFont(self._title_font)
250
+
251
+ # # Calculate horizontal and vertical center positions
252
+ # horizontal_center = (self.boundingRect().width(
253
+ # ) - self.title_item.boundingRect().width()) / 2
254
+ # vertical_center = (self.title_height -
255
+ # self.title_item.boundingRect().height()) / 2
256
+
257
+ # self.title_item.setPos(horizontal_center, vertical_center)
258
+
259
+ return
260
+
261
+ def setDetailState(self, state: Mapping[str, bool]) -> None:
262
+ """Apply a resolved per-element detail state.
263
+
264
+ The scene resolves its detail policy into a mapping of element key to
265
+ "draw it" and hands the whole mapping over, so an application can add
266
+ elements of its own to the same broadcast and read them back here.
267
+ The zoom state itself rides along under the ``detail`` key.
268
+
269
+ Every child is pushed, not only the ones whose flag changed: a socket
270
+ or a content proxy built after the last flip has never seen the state.
271
+ """
272
+ self._detail_visible = bool(state.get("detail", self._detail_visible))
273
+ self._content_visible = bool(state.get("content", True))
274
+ self._sockets_visible = bool(state.get("sockets", True))
275
+ self._label_enabled = bool(state.get("label", True))
276
+
277
+ self._refreshLabelVisibility()
278
+
279
+ # The content proxy, the sockets and any other child item that wants to
280
+ # fade out too.
281
+ for child in self.childItems():
282
+ self._applyDetailToChild(child)
283
+
284
+ def setDetailVisible(self, visible: bool) -> None:
285
+ """Show or hide this node's fine detail at low zoom.
286
+
287
+ The legacy entry point, kept for callers outside the scene: it drops
288
+ the content widget, the floating label and the socket circles together.
289
+ The scene uses :meth:`setDetailState` instead, which resolves each of
290
+ those separately.
291
+ """
292
+ visible = bool(visible)
293
+ self.setDetailState(
294
+ {
295
+ "detail": visible,
296
+ "content": visible,
297
+ "sockets": visible,
298
+ "label": visible,
299
+ }
300
+ )
301
+
302
+ def _applyDetailToChild(self, child) -> None:
303
+ """Push this node's detail state onto one child item.
304
+
305
+ Sockets own a ``setDetailVisible``; the content proxy widget does not,
306
+ so it is matched by identity against ``grContent``.
307
+ """
308
+ setter = getattr(child, "setDetailVisible", None)
309
+ if setter is not None:
310
+ setter(self._sockets_visible)
311
+ elif child is getattr(self, "grContent", None):
312
+ child.setVisible(self._content_visible)
313
+
314
+ def syncDetailVisibility(self) -> None:
315
+ """Push the scene's current detail state onto every child item.
316
+
317
+ The sockets and the content proxy are built during ``Node.__init__``,
318
+ after Qt already reported this node joining the scene, so the scene's
319
+ one-off broadcast never reached them - and Qt's
320
+ ``ItemChildAddedChange`` hands over a base-class wrapper for an item
321
+ still inside its own constructor, which has no ``setDetailVisible``.
322
+ Called from ``Node.__init__`` once the node is fully built.
323
+ """
324
+ scene = self.scene()
325
+ state = getattr(scene, "detail_state", None)
326
+ if callable(state):
327
+ self.setDetailState(state())
328
+ return
329
+ self._detail_visible = getattr(scene, "_detail_visible", True)
330
+ self._refreshLabelVisibility()
331
+ for child in self.childItems():
332
+ self._applyDetailToChild(child)
333
+
334
+ def initContent(self) -> None:
335
+ """Set up the `grContent` - ``QGraphicsProxyWidget`` to have a container for `Graphics Content`"""
336
+ if self.content is not None:
337
+ self.content.setGeometry(self.edge_padding, self.title_height + self.edge_padding,
338
+ self.width - 2 * self.edge_padding, self.height - 2 * self.edge_padding - self.title_height)
339
+
340
+ # get the QGraphicsProxyWidget when inserted into the grScene
341
+ self.grContent = self.node.scene.grScene.addWidget(self.content)
342
+ self.grContent.node = self.node
343
+ self.grContent.setParentItem(self)
344
+
345
+ def initLabel(self) -> None:
346
+ """Create the floating single-line label above the node (hidden until text is set)."""
347
+ self.label_item = QDMGraphicsNodeLabel(self, self)
348
+ self.label_item.setFont(self._label_font)
349
+ self.label_item.setDefaultTextColor(self._label_color)
350
+ self._label_visible = False
351
+ self.label_item.setVisible(False)
352
+ self._updateLabelPos()
353
+
354
+ def setLabelText(self, text: str) -> None:
355
+ """Set label text (single-line). Empty text hides the label. No history stored."""
356
+ text = QDMGraphicsNodeLabel.sanitize(text)
357
+ self.label_item.setPlainText(text)
358
+ if text:
359
+ self._label_visible = True
360
+ self._refreshLabelVisibility()
361
+ self._updateLabelPos()
362
+
363
+ def labelText(self) -> str:
364
+ """Return current label text."""
365
+ try:
366
+ return self.label_item.toPlainText()
367
+ except Exception:
368
+ return ""
369
+
370
+ def setLabelVisible(self, visible: bool) -> None:
371
+ """Show/hide the label. Hidden when text is empty regardless."""
372
+ self._label_visible = bool(visible)
373
+ self._refreshLabelVisibility()
374
+
375
+ def isLabelVisible(self) -> bool:
376
+ """``True`` while the label is requested visible and carries text."""
377
+ return bool(self._label_visible and bool(self.labelText()))
378
+
379
+ def setLabelOffset(self, offset: float) -> None:
380
+ """Set the gap in px between node top and label bottom.
381
+
382
+ :param offset: the new gap in px
383
+ :type offset: ``float``
384
+ """
385
+ self.label_offset = float(offset)
386
+ self._updateLabelPos()
387
+
388
+ def labelOffset(self) -> float:
389
+ """Return the gap in px between node top and label bottom."""
390
+ return float(self.label_offset)
391
+
392
+ def setLabelColor(self, color) -> None:
393
+ """Set the text color of the label.
394
+
395
+ :param color: the new text color
396
+ :type color: QColor
397
+ """
398
+ self._label_color = QColor(color) if not isinstance(color, QColor) else color
399
+ self.label_item.setDefaultTextColor(self._label_color)
400
+
401
+ def setLabelFont(self, font: QFont) -> None:
402
+ """Set the font of the label and move the label to its new position.
403
+
404
+ :param font: the new font
405
+ :type font: QFont
406
+ """
407
+ self._label_font = font
408
+ self.label_item.setFont(font)
409
+ self._updateLabelPos()
410
+
411
+ def setLabelBackground(self, bg, border=None) -> None:
412
+ """Set the background and border colors of the label box.
413
+
414
+ :param bg: background color of the label box
415
+ :type bg: QColor
416
+ :param border: border color of the label box
417
+ :type border: QColor
418
+ """
419
+ bg = QColor(bg) if not isinstance(bg, QColor) else bg
420
+ b = QColor(border) if border is not None and not isinstance(border, QColor) else border
421
+ self.label_item.setLabelColors(bg=bg, border=b)
422
+
423
+ def _refreshLabelVisibility(self) -> None:
424
+ try:
425
+ self.label_item.setVisible(bool(
426
+ self._label_visible
427
+ and bool(self.label_item.toPlainText())
428
+ and self._label_enabled))
429
+ except Exception:
430
+ pass
431
+
432
+ def _updateLabelPos(self) -> None:
433
+ try:
434
+ w = self.label_item.boundingRect().width()
435
+ h = self.label_item.boundingRect().height()
436
+ self.label_item.setPos((self.width - w) / 2, -h - self.label_offset)
437
+ except Exception:
438
+ pass
439
+
440
+ def _onLabelEdited(self, text: str) -> None:
441
+ text = QDMGraphicsNodeLabel.sanitize(text)
442
+ if text:
443
+ self._label_visible = True
444
+ self._refreshLabelVisibility()
445
+ self._updateLabelPos()
446
+ try:
447
+ self.node.labelChanged.emit(text)
448
+ except Exception:
449
+ pass
450
+ try:
451
+ self.node.scene.history.storeHistory("Node label changed", setModified=True)
452
+ except Exception:
453
+ pass
454
+
455
+ # def initIcon(self):
456
+ # """Set up the icon Graphics representation"""
457
+ # print("setting icons")
458
+
459
+ # if self.icon_path:
460
+ # print("ICON:", self.icon_path)
461
+ # self.icon_item = QGraphicsPixmapItem(self.icon_path, self)
462
+ # icon_rect = self.icon_item.boundingRect()
463
+ # icon_center_x = (self.width - icon_rect.width()) / 2
464
+ # icon_center_y = (self.height - icon_rect.height()) / 2
465
+ # self.icon_item.setPos(icon_center_x, icon_center_y)
466
+
467
+ def paint(self, painter, QStyleOptionGraphicsItem, widget=None) -> None:
468
+ """Painting the rounded rectanglar `Node`"""
469
+ # title
470
+ # path_title = QPainterPath()
471
+ # path_title.setFillRule(Qt.WindingFill)
472
+ # path_title.addRoundedRect(
473
+ # 0, 0, self.width, self.title_height, self.edge_roundness, self.edge_roundness)
474
+ # path_title.addRect(0, self.title_height - self.edge_roundness,
475
+ # self.edge_roundness, self.edge_roundness)
476
+ # path_title.addRect(self.width - self.edge_roundness, self.title_height -
477
+ # self.edge_roundness, self.edge_roundness, self.edge_roundness)
478
+ # painter.setPen(Qt.NoPen)
479
+ # painter.setBrush(self._brush_title)
480
+ # painter.drawPath(path_title.simplified())
481
+
482
+ # # content
483
+ # path_content = QPainterPath()
484
+ # path_content.setFillRule(Qt.WindingFill)
485
+ # path_content.addRoundedRect(0, self.title_height, self.width, self.height -
486
+ # self.title_height, self.edge_roundness, self.edge_roundness)
487
+ # path_content.addRect(0, self.title_height,
488
+ # self.edge_roundness, self.edge_roundness)
489
+ # path_content.addRect(self.width - self.edge_roundness,
490
+ # self.title_height, self.edge_roundness, self.edge_roundness)
491
+ # painter.setPen(Qt.NoPen)
492
+ # painter.setBrush(self._brush_background)
493
+ # painter.drawPath(path_content.simplified())
494
+
495
+ # outline
496
+ # path_outline = QPainterPath()
497
+ # # path_outline.addRoundedRect(-1, -1, self.width+2,
498
+ # # self.height+2, self.edge_roundness, self.edge_roundness)
499
+ # painter.setBrush(Qt.NoBrush)
500
+ # if self.hovered:
501
+ # path_outline.addRoundedRect(-1, -1, self.width + 2,
502
+ # self.height + 2, self.edge_roundness, self.edge_roundness)
503
+ # painter.setPen(self._pen_hovered)
504
+ # painter.drawPath(path_outline.simplified())
505
+ # painter.setPen(self._pen_default)
506
+ # painter.drawPath(path_outline.simplified())
507
+ # else:
508
+ # path_outline.addRoundedRect(-1, -1, self.width + 2,
509
+ # self.height + 2, self.edge_roundness, self.edge_roundness)
510
+ # painter.setPen(
511
+ # self._pen_default if not self.isSelected() else self._pen_selected)
512
+ # painter.drawPath(path_outline.simplified())
513
+
514
+ if self.hovered or self.isSelected():
515
+ path_outline = QPainterPath()
516
+ path_outline.addRoundedRect(-1, -1, self.width + 2,
517
+ self.height + 2, self.edge_roundness, self.edge_roundness)
518
+ painter.setBrush(Qt.NoBrush)
519
+ if self.hovered:
520
+ painter.setPen(self._pen_hovered)
521
+ else:
522
+ painter.setPen(self._pen_selected if self.isSelected()
523
+ else self._pen_default)
524
+ painter.drawPath(path_outline.simplified())