drawsvg-ui 0.4.0__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.
palette.py ADDED
@@ -0,0 +1,556 @@
1
+ from PySide6 import QtCore, QtGui, QtWidgets
2
+ import math
3
+
4
+ from constants import DEFAULTS, DEFAULT_FILL, PALETTE_MIME, PEN_NORMAL, SHAPES
5
+ from items import CurvyBracketItem, build_curvy_bracket_path
6
+
7
+ def _fit_rect_to_ratio(rect: QtCore.QRectF, aspect_ratio: float) -> QtCore.QRectF:
8
+ """Return a copy of *rect* scaled to match the requested aspect ratio."""
9
+
10
+ if aspect_ratio <= 0:
11
+ return QtCore.QRectF(rect)
12
+
13
+ width = rect.width()
14
+ height = rect.height()
15
+ if width <= 0 or height <= 0:
16
+ return QtCore.QRectF(rect)
17
+
18
+ current_ratio = width / height
19
+ new_width = width
20
+ new_height = height
21
+ if current_ratio > aspect_ratio:
22
+ new_width = height * aspect_ratio
23
+ else:
24
+ new_height = width / aspect_ratio
25
+
26
+ fitted = QtCore.QRectF(
27
+ rect.center().x() - new_width / 2,
28
+ rect.center().y() - new_height / 2,
29
+ new_width,
30
+ new_height,
31
+ )
32
+ return fitted
33
+
34
+ def _build_shape_icon(
35
+ name: str,
36
+ size: QtCore.QSize,
37
+ *,
38
+ device_pixel_ratio: float = 1.0,
39
+ ) -> QtGui.QPixmap:
40
+ device_pixel_ratio = max(1.0, float(device_pixel_ratio))
41
+ pixel_size = QtCore.QSize(
42
+ max(1, int(math.ceil(size.width() * device_pixel_ratio))),
43
+ max(1, int(math.ceil(size.height() * device_pixel_ratio))),
44
+ )
45
+ pixmap = QtGui.QPixmap(pixel_size)
46
+ pixmap.setDevicePixelRatio(device_pixel_ratio)
47
+ pixmap.fill(QtCore.Qt.GlobalColor.transparent)
48
+
49
+ painter = QtGui.QPainter(pixmap)
50
+ painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
51
+ painter.setPen(PEN_NORMAL)
52
+ painter.setBrush(DEFAULT_FILL)
53
+
54
+ padding = 4 * device_pixel_ratio
55
+ rect = QtCore.QRectF(
56
+ padding,
57
+ padding,
58
+ pixel_size.width() - 2 * padding,
59
+ pixel_size.height() - 2 * padding,
60
+ )
61
+
62
+ lower_name = name.lower()
63
+ if lower_name == "rectangle":
64
+ dims = DEFAULTS.get(name)
65
+ draw_rect = rect
66
+ if dims and dims[1]:
67
+ draw_rect = _fit_rect_to_ratio(rect, dims[0] / dims[1])
68
+ painter.drawRect(draw_rect)
69
+ elif lower_name == "rounded rectangle":
70
+ dims = DEFAULTS.get(name)
71
+ draw_rect = rect
72
+ if dims and dims[1]:
73
+ draw_rect = _fit_rect_to_ratio(rect, dims[0] / dims[1])
74
+ radius = 8.0 * device_pixel_ratio
75
+ painter.drawRoundedRect(draw_rect, radius, radius)
76
+ elif lower_name == "split rounded rectangle":
77
+ dims = DEFAULTS.get(name)
78
+ draw_rect = rect
79
+ if dims and dims[1]:
80
+ draw_rect = _fit_rect_to_ratio(rect, dims[0] / dims[1])
81
+ radius = 8.0 * device_pixel_ratio
82
+
83
+ base_path = QtGui.QPainterPath()
84
+ base_path.addRoundedRect(draw_rect, radius, radius)
85
+ painter.fillPath(base_path, DEFAULT_FILL)
86
+
87
+ header_height = draw_rect.height() / 3.0
88
+ top_clip = QtGui.QPainterPath()
89
+ top_clip.addRect(
90
+ draw_rect.left(),
91
+ draw_rect.top(),
92
+ draw_rect.width(),
93
+ header_height,
94
+ )
95
+ header_color = QtGui.QColor("#f6e3b0")
96
+ painter.fillPath(base_path.intersected(top_clip), header_color)
97
+
98
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
99
+ painter.setPen(PEN_NORMAL)
100
+ painter.drawRoundedRect(draw_rect, radius, radius)
101
+
102
+ line_y = draw_rect.top() + header_height
103
+ divider_pen = QtGui.QPen(QtGui.QColor("#777"))
104
+ divider_pen.setWidthF(max(1.0, PEN_NORMAL.widthF() * device_pixel_ratio * 0.9))
105
+ painter.setPen(divider_pen)
106
+ painter.drawLine(draw_rect.left(), line_y, draw_rect.right(), line_y)
107
+
108
+ handle_radius = 3.0 * device_pixel_ratio
109
+ painter.setBrush(QtGui.QColor("#d28b00"))
110
+ painter.setPen(QtGui.QPen(QtCore.Qt.PenStyle.NoPen))
111
+ painter.drawEllipse(QtCore.QPointF(draw_rect.center().x(), line_y), handle_radius, handle_radius)
112
+
113
+ painter.setPen(PEN_NORMAL)
114
+ painter.setBrush(DEFAULT_FILL)
115
+ elif lower_name == "ellipse":
116
+ dims = DEFAULTS.get(name)
117
+ ellipse_rect = rect
118
+ if dims and dims[1]:
119
+ ellipse_rect = _fit_rect_to_ratio(rect, dims[0] / dims[1])
120
+ painter.drawEllipse(ellipse_rect)
121
+ elif lower_name == "circle":
122
+ diameter = min(rect.width(), rect.height())
123
+ circle_rect = QtCore.QRectF(
124
+ rect.center().x() - diameter / 2,
125
+ rect.center().y() - diameter / 2,
126
+ diameter,
127
+ diameter,
128
+ )
129
+ painter.drawEllipse(circle_rect)
130
+ elif lower_name == "triangle":
131
+ points = QtGui.QPolygonF(
132
+ [
133
+ QtCore.QPointF(rect.center().x(), rect.top()),
134
+ QtCore.QPointF(rect.left(), rect.bottom()),
135
+ QtCore.QPointF(rect.right(), rect.bottom()),
136
+ ]
137
+ )
138
+ painter.drawPolygon(points)
139
+ elif lower_name == "diamond":
140
+ points = QtGui.QPolygonF(
141
+ [
142
+ QtCore.QPointF(rect.center().x(), rect.top()),
143
+ QtCore.QPointF(rect.right(), rect.center().y()),
144
+ QtCore.QPointF(rect.center().x(), rect.bottom()),
145
+ QtCore.QPointF(rect.left(), rect.center().y()),
146
+ ]
147
+ )
148
+ painter.drawPolygon(points)
149
+ elif lower_name == "line":
150
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
151
+ y = rect.center().y()
152
+ painter.drawLine(rect.left(), y, rect.right(), y)
153
+ elif lower_name == "arrow":
154
+ shaft_end_x = rect.right() - rect.width() * 0.25
155
+ center_y = rect.center().y()
156
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
157
+ painter.drawLine(rect.left(), center_y, shaft_end_x, center_y)
158
+
159
+ arrow_height = rect.height() * 0.4
160
+ painter.setBrush(DEFAULT_FILL)
161
+ arrow_head = QtGui.QPolygonF(
162
+ [
163
+ QtCore.QPointF(rect.right(), center_y),
164
+ QtCore.QPointF(shaft_end_x, center_y - arrow_height / 2),
165
+ QtCore.QPointF(shaft_end_x, center_y + arrow_height / 2),
166
+ ]
167
+ )
168
+ painter.drawPolygon(arrow_head)
169
+ elif lower_name == "block arrow":
170
+ dims = DEFAULTS.get(name)
171
+ draw_rect = rect
172
+ if dims and dims[1]:
173
+ draw_rect = _fit_rect_to_ratio(rect, dims[0] / dims[1])
174
+
175
+ head_frac = 0.32
176
+ head_width = draw_rect.width() * head_frac
177
+ shaft_frac = 0.45
178
+ shaft_height = draw_rect.height() * shaft_frac
179
+ shaft_top = draw_rect.center().y() - shaft_height / 2.0
180
+ shaft_bottom = draw_rect.center().y() + shaft_height / 2.0
181
+ head_base_x = draw_rect.right() - head_width
182
+
183
+ polygon = QtGui.QPolygonF(
184
+ [
185
+ QtCore.QPointF(draw_rect.left(), shaft_top),
186
+ QtCore.QPointF(head_base_x, shaft_top),
187
+ QtCore.QPointF(head_base_x, draw_rect.top()),
188
+ QtCore.QPointF(draw_rect.right(), draw_rect.center().y()),
189
+ QtCore.QPointF(head_base_x, draw_rect.bottom()),
190
+ QtCore.QPointF(head_base_x, shaft_bottom),
191
+ QtCore.QPointF(draw_rect.left(), shaft_bottom),
192
+ ]
193
+ )
194
+ painter.drawPolygon(polygon)
195
+
196
+ painter.setPen(QtGui.QPen(QtCore.Qt.PenStyle.NoPen))
197
+ painter.setBrush(QtGui.QColor("#d28b00"))
198
+ handle_radius = 3.0 * device_pixel_ratio
199
+ painter.drawEllipse(
200
+ QtCore.QPointF(head_base_x, shaft_top), handle_radius, handle_radius
201
+ )
202
+ tail_mid_x = draw_rect.left() + (head_base_x - draw_rect.left()) / 2.0
203
+ painter.drawEllipse(
204
+ QtCore.QPointF(tail_mid_x, shaft_bottom), handle_radius, handle_radius
205
+ )
206
+ painter.setBrush(DEFAULT_FILL)
207
+ painter.setPen(PEN_NORMAL)
208
+ elif lower_name == "folder tree":
209
+ painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True)
210
+
211
+ branch_pen = QtGui.QPen(QtGui.QColor("#7a7a7a"))
212
+ branch_pen.setWidthF(max(1.0, PEN_NORMAL.widthF() * device_pixel_ratio * 0.7))
213
+ branch_pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap)
214
+ painter.setPen(branch_pen)
215
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
216
+
217
+ padding_x = rect.width() * 0.12
218
+ padding_y = rect.height() * 0.18
219
+ indent = rect.width() * 0.28
220
+ line_gap = rect.height() * 0.26
221
+
222
+ base_x = rect.left() + padding_x
223
+ level1_x = base_x + indent
224
+ level2_x = level1_x + indent
225
+
226
+ y0 = rect.top() + padding_y
227
+ y1 = y0 + line_gap
228
+ y2 = y1 + line_gap
229
+
230
+ painter.drawLine(QtCore.QPointF(base_x, y0), QtCore.QPointF(base_x, y2))
231
+ painter.drawLine(QtCore.QPointF(base_x, y0), QtCore.QPointF(level1_x, y0))
232
+ painter.drawLine(QtCore.QPointF(base_x, y1), QtCore.QPointF(level1_x, y1))
233
+ painter.drawLine(QtCore.QPointF(level1_x, y1), QtCore.QPointF(level2_x, y1))
234
+ painter.drawLine(QtCore.QPointF(base_x, y2), QtCore.QPointF(level1_x, y2))
235
+ painter.drawLine(QtCore.QPointF(level1_x, y2), QtCore.QPointF(level2_x, y2))
236
+
237
+ dot_radius = 3.0 * device_pixel_ratio
238
+ dot_color = QtGui.QColor("#f28c28")
239
+ painter.setPen(QtGui.QPen(QtCore.Qt.PenStyle.NoPen))
240
+ painter.setBrush(dot_color)
241
+ dot_centers = [
242
+ QtCore.QPointF(base_x, y0),
243
+ QtCore.QPointF(base_x, y1),
244
+ QtCore.QPointF(base_x, y2),
245
+ QtCore.QPointF(level1_x, y1),
246
+ QtCore.QPointF(level1_x, y2),
247
+ QtCore.QPointF(level2_x, y1),
248
+ QtCore.QPointF(level2_x, y2),
249
+ ]
250
+ for center in dot_centers:
251
+ painter.drawEllipse(center, dot_radius, dot_radius)
252
+
253
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
254
+ label_pen = QtGui.QPen(QtGui.QColor("#d7d7d7"))
255
+ label_pen.setWidthF(max(1.0, PEN_NORMAL.widthF() * device_pixel_ratio * 0.6))
256
+ painter.setPen(label_pen)
257
+
258
+ label_len = rect.width() * 0.32
259
+ text_offsets = [
260
+ (level1_x + dot_radius * 2.0 + 3.0, y0),
261
+ (level2_x + dot_radius * 2.0 + 3.0, y1),
262
+ (level2_x + dot_radius * 2.0 + 3.0, y2),
263
+ ]
264
+ for tx, ty in text_offsets:
265
+ painter.drawLine(QtCore.QPointF(tx, ty), QtCore.QPointF(tx + label_len, ty))
266
+
267
+ painter.setPen(PEN_NORMAL)
268
+ painter.setBrush(DEFAULT_FILL)
269
+ elif lower_name == "curvy right bracket":
270
+ dims = DEFAULTS.get(name)
271
+ draw_rect = rect
272
+ if dims and dims[1]:
273
+ draw_rect = _fit_rect_to_ratio(rect, dims[0] / dims[1])
274
+ path = build_curvy_bracket_path(
275
+ draw_rect.width(),
276
+ draw_rect.height(),
277
+ draw_rect.height() * CurvyBracketItem.DEFAULT_HOOK_RATIO,
278
+ )
279
+ path.translate(draw_rect.left(), draw_rect.top())
280
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
281
+ painter.drawPath(path)
282
+ painter.setBrush(DEFAULT_FILL)
283
+ elif lower_name == "text":
284
+ radius = 6 * device_pixel_ratio
285
+ painter.drawRoundedRect(rect, radius, radius)
286
+
287
+ inner_rect = rect.adjusted(
288
+ rect.width() * 0.18,
289
+ rect.height() * 0.18,
290
+ -rect.width() * 0.18,
291
+ -rect.height() * 0.18,
292
+ )
293
+ painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
294
+ painter.drawLine(inner_rect.left(), inner_rect.top(), inner_rect.right(), inner_rect.top())
295
+ painter.drawLine(
296
+ inner_rect.center().x(),
297
+ inner_rect.top(),
298
+ inner_rect.center().x(),
299
+ inner_rect.bottom(),
300
+ )
301
+ else:
302
+ painter.drawRoundedRect(rect, 6 * device_pixel_ratio, 6 * device_pixel_ratio)
303
+
304
+ painter.end()
305
+ return pixmap
306
+
307
+ class PaletteList(QtWidgets.QListWidget):
308
+ shapeClicked = QtCore.Signal(str)
309
+
310
+ def __init__(self, parent=None):
311
+ super().__init__(parent)
312
+ self.setDragEnabled(False)
313
+ self.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection)
314
+ self.setViewMode(QtWidgets.QListView.ViewMode.IconMode)
315
+
316
+ self._base_icon_size = QtCore.QSize(56, 56)
317
+ self._base_cell_padding = 4
318
+ self._base_spacing = 6
319
+ self._last_device_pixel_ratio: float | None = None
320
+ self._current_screen: QtGui.QScreen | None = None
321
+ self._window_handle_connected = False
322
+
323
+ self._update_metrics()
324
+
325
+ self.setResizeMode(QtWidgets.QListView.ResizeMode.Adjust)
326
+ self.setMouseTracking(True)
327
+ self.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
328
+
329
+ self._pressed_item: QtWidgets.QListWidgetItem | None = None
330
+ self._press_pos = QtCore.QPointF()
331
+ self._hovered_item: QtWidgets.QListWidgetItem | None = None
332
+ self._hover_brush = QtGui.QBrush(QtGui.QColor("#d2e7ff"))
333
+
334
+ for name in SHAPES:
335
+ item = QtWidgets.QListWidgetItem("")
336
+ item.setData(QtCore.Qt.ItemDataRole.UserRole, name)
337
+ item.setToolTip(name)
338
+ self.addItem(item)
339
+
340
+ self._refresh_icons(force=True)
341
+
342
+ def showEvent(self, event: QtGui.QShowEvent) -> None:
343
+ super().showEvent(event)
344
+ self._bind_to_current_screen()
345
+ self._refresh_icons()
346
+
347
+ def event(self, event: QtCore.QEvent) -> bool:
348
+ if event.type() in (
349
+ QtCore.QEvent.Type.DevicePixelRatioChange,
350
+ QtCore.QEvent.Type.ScreenChangeInternal,
351
+ ):
352
+ self._bind_to_current_screen()
353
+ self._refresh_icons(force=True)
354
+ return super().event(event)
355
+
356
+ def _update_metrics(self) -> None:
357
+ icon_size = QtCore.QSize(self._base_icon_size)
358
+ self.setIconSize(icon_size)
359
+ padding = int(self._base_cell_padding)
360
+ self.setGridSize(
361
+ QtCore.QSize(
362
+ icon_size.width() + padding * 2,
363
+ icon_size.height() + padding * 2,
364
+ )
365
+ )
366
+ self.setSpacing(int(self._base_spacing))
367
+
368
+ def _effective_device_pixel_ratio(self) -> float:
369
+ dpr = float(self.devicePixelRatioF())
370
+ if not math.isfinite(dpr) or dpr <= 0.0:
371
+ handle = self.windowHandle()
372
+ if handle is not None:
373
+ try:
374
+ dpr = float(handle.devicePixelRatio())
375
+ except AttributeError:
376
+ dpr = 1.0
377
+ if not math.isfinite(dpr) or dpr <= 0.0:
378
+ screen = self.screen() or QtGui.QGuiApplication.primaryScreen()
379
+ if screen is not None:
380
+ dpr = float(screen.devicePixelRatio())
381
+ return dpr if math.isfinite(dpr) and dpr > 0.0 else 1.0
382
+
383
+ def _refresh_icons(self, *, force: bool = False) -> None:
384
+ if self.count() == 0:
385
+ return
386
+ dpr = self._effective_device_pixel_ratio()
387
+ if (
388
+ not force
389
+ and self._last_device_pixel_ratio is not None
390
+ and abs(self._last_device_pixel_ratio - dpr) < 1e-3
391
+ ):
392
+ return
393
+ self._last_device_pixel_ratio = dpr
394
+ self._update_metrics()
395
+ icon_size = self.iconSize()
396
+ for index in range(self.count()):
397
+ item = self.item(index)
398
+ shape = self._shape_from_item(item)
399
+ if not shape:
400
+ continue
401
+ pixmap = _build_shape_icon(
402
+ shape,
403
+ icon_size,
404
+ device_pixel_ratio=dpr,
405
+ )
406
+ item.setIcon(QtGui.QIcon(pixmap))
407
+
408
+ def _ensure_window_handle_connection(self) -> None:
409
+ if self._window_handle_connected:
410
+ return
411
+ window = self.window().windowHandle() if self.window() is not None else None
412
+ if window is None:
413
+ window = self.windowHandle()
414
+ if window is None:
415
+ return
416
+ window.screenChanged.connect(self._on_window_screen_changed)
417
+ self._window_handle_connected = True
418
+
419
+ def _bind_to_current_screen(self) -> None:
420
+ self._ensure_window_handle_connection()
421
+ screen = self.screen()
422
+ if screen is None:
423
+ window = self.window().windowHandle() if self.window() is not None else None
424
+ if window is None:
425
+ window = self.windowHandle()
426
+ if window is not None:
427
+ screen = window.screen()
428
+ self._connect_to_screen(screen)
429
+
430
+ def _connect_to_screen(self, screen: QtGui.QScreen | None) -> None:
431
+ current = self._current_screen
432
+ if current is screen:
433
+ return
434
+ if current is not None:
435
+ try:
436
+ current.logicalDotsPerInchChanged.disconnect(self._on_screen_metrics_changed)
437
+ except (TypeError, RuntimeError):
438
+ pass
439
+ if hasattr(current, "devicePixelRatioChanged"):
440
+ try:
441
+ getattr(current, "devicePixelRatioChanged").disconnect(self._on_screen_metrics_changed)
442
+ except (TypeError, RuntimeError):
443
+ pass
444
+ self._current_screen = screen
445
+ if screen is None:
446
+ return
447
+ screen.logicalDotsPerInchChanged.connect(self._on_screen_metrics_changed)
448
+ if hasattr(screen, "devicePixelRatioChanged"):
449
+ getattr(screen, "devicePixelRatioChanged").connect(self._on_screen_metrics_changed)
450
+
451
+ def _on_window_screen_changed(self, screen: QtGui.QScreen | None) -> None:
452
+ self._connect_to_screen(screen)
453
+ self._refresh_icons(force=True)
454
+
455
+ def _on_screen_metrics_changed(self, *args) -> None:
456
+ self._refresh_icons(force=True)
457
+
458
+ def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
459
+ self._update_hover_from_pos(event.position())
460
+ if event.button() == QtCore.Qt.MouseButton.LeftButton:
461
+ item = self.itemAt(event.position().toPoint())
462
+ if item is not None:
463
+ self._pressed_item = item
464
+ self._press_pos = event.position()
465
+ event.accept()
466
+ return
467
+ self._pressed_item = None
468
+ super().mousePressEvent(event)
469
+
470
+ def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
471
+ self._update_hover_from_pos(event.position())
472
+ if (
473
+ self._pressed_item is not None
474
+ and event.buttons() & QtCore.Qt.MouseButton.LeftButton
475
+ ):
476
+ delta = event.position() - self._press_pos
477
+ distance = QtCore.QLineF(QtCore.QPointF(), delta).length()
478
+ if distance >= QtWidgets.QApplication.startDragDistance():
479
+ item = self._pressed_item
480
+ self._pressed_item = None
481
+ if item is not None:
482
+ self._start_drag_from_item(item)
483
+ event.accept()
484
+ return
485
+ super().mouseMoveEvent(event)
486
+
487
+ def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
488
+ self._update_hover_from_pos(event.position())
489
+ if event.button() == QtCore.Qt.MouseButton.LeftButton and self._pressed_item:
490
+ item = self.itemAt(event.position().toPoint())
491
+ if item is self._pressed_item:
492
+ shape = self._shape_from_item(item)
493
+ if shape:
494
+ self.shapeClicked.emit(shape)
495
+ self._pressed_item = None
496
+ event.accept()
497
+ return
498
+ self._pressed_item = None
499
+ super().mouseReleaseEvent(event)
500
+
501
+ def _shape_from_item(self, item: QtWidgets.QListWidgetItem | None) -> str | None:
502
+ if item is None:
503
+ return None
504
+ data = item.data(QtCore.Qt.ItemDataRole.UserRole)
505
+ return data if isinstance(data, str) else None
506
+
507
+ def _start_drag_from_item(self, item: QtWidgets.QListWidgetItem) -> None:
508
+ shape = self._shape_from_item(item)
509
+ if not shape:
510
+ return
511
+
512
+ drag = QtGui.QDrag(self)
513
+ md = QtCore.QMimeData()
514
+ md.setData(PALETTE_MIME, shape.encode("utf-8"))
515
+ md.setText(shape)
516
+ drag.setMimeData(md)
517
+
518
+ self._set_hover_item(None)
519
+
520
+ icon = item.icon()
521
+ if not icon.isNull():
522
+ size = self.iconSize()
523
+ pix = icon.pixmap(size)
524
+ if not pix.isNull():
525
+ drag.setPixmap(pix)
526
+ drag.setHotSpot(QtCore.QPoint(pix.width() // 2, pix.height() // 2))
527
+ drag.exec(QtCore.Qt.DropAction.CopyAction)
528
+
529
+ def leaveEvent(self, event: QtCore.QEvent) -> None:
530
+ self._set_hover_item(None)
531
+ super().leaveEvent(event)
532
+
533
+ def enterEvent(self, event: QtCore.QEvent) -> None:
534
+ cursor_pos = self.viewport().mapFromGlobal(QtGui.QCursor.pos())
535
+ self._update_hover_from_pos(QtCore.QPointF(cursor_pos))
536
+ super().enterEvent(event)
537
+
538
+ def _update_hover_from_pos(self, pos: QtCore.QPointF) -> None:
539
+ item = self.itemAt(pos.toPoint())
540
+ if item is not self._hovered_item:
541
+ self._set_hover_item(item)
542
+
543
+ def _set_hover_item(self, item: QtWidgets.QListWidgetItem | None) -> None:
544
+ if item is self._hovered_item:
545
+ return
546
+
547
+ if self._hovered_item is not None:
548
+ self._hovered_item.setData(QtCore.Qt.ItemDataRole.BackgroundRole, None)
549
+
550
+ self._hovered_item = item
551
+
552
+ if self._hovered_item is not None:
553
+ self._hovered_item.setData(
554
+ QtCore.Qt.ItemDataRole.BackgroundRole, self._hover_brush
555
+ )
556
+