QT-PyQt-PySide-Custom-Widgets-Pro 1.1.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.
Files changed (45) hide show
  1. custom_widgets_pro/__init__.py +73 -0
  2. custom_widgets_pro/_catalog.py +25 -0
  3. custom_widgets_pro/_cli.py +132 -0
  4. custom_widgets_pro/_license.py +419 -0
  5. custom_widgets_pro/_version.py +14 -0
  6. custom_widgets_pro/datatable/__init__.py +17 -0
  7. custom_widgets_pro/datatable/datatable_pro.py +319 -0
  8. custom_widgets_pro/datatable/export.py +143 -0
  9. custom_widgets_pro/datatable/frozen_view.py +112 -0
  10. custom_widgets_pro/datatable/grouping.py +309 -0
  11. custom_widgets_pro/datatable/pivot.py +100 -0
  12. custom_widgets_pro/datatable/provider.py +98 -0
  13. custom_widgets_pro/datatable/virtual_model.py +451 -0
  14. custom_widgets_pro/widgets/__init__.py +7 -0
  15. custom_widgets_pro/widgets/charts/QCustomBeeswarm.py +317 -0
  16. custom_widgets_pro/widgets/charts/QCustomBubbleChart.py +655 -0
  17. custom_widgets_pro/widgets/charts/QCustomCandlestickChart.py +522 -0
  18. custom_widgets_pro/widgets/charts/QCustomDivergingBarChart.py +430 -0
  19. custom_widgets_pro/widgets/charts/QCustomDotMatrix.py +266 -0
  20. custom_widgets_pro/widgets/charts/QCustomFunnelChart.py +474 -0
  21. custom_widgets_pro/widgets/charts/QCustomGanttChart.py +376 -0
  22. custom_widgets_pro/widgets/charts/QCustomHeatmap.py +555 -0
  23. custom_widgets_pro/widgets/charts/QCustomRadarChart.py +600 -0
  24. custom_widgets_pro/widgets/charts/QCustomRadialBars.py +441 -0
  25. custom_widgets_pro/widgets/charts/QCustomRadialLines.py +554 -0
  26. custom_widgets_pro/widgets/charts/QCustomRangeBarChart.py +504 -0
  27. custom_widgets_pro/widgets/charts/QCustomSankey.py +539 -0
  28. custom_widgets_pro/widgets/charts/QCustomScatterChart.py +623 -0
  29. custom_widgets_pro/widgets/charts/__init__.py +1 -0
  30. custom_widgets_pro/widgets/data/QCustomCodeEditor.py +391 -0
  31. custom_widgets_pro/widgets/data/QCustomNodeGraph.py +1030 -0
  32. custom_widgets_pro/widgets/data/QCustomRichTextEditor.py +189 -0
  33. custom_widgets_pro/widgets/data/QCustomTableToolbar.py +566 -0
  34. custom_widgets_pro/widgets/data/__init__.py +1 -0
  35. custom_widgets_pro/widgets/media/QCustomImageViewer.py +300 -0
  36. custom_widgets_pro/widgets/media/QCustomMediaGrid.py +197 -0
  37. custom_widgets_pro/widgets/media/QCustomMediaTimeline.py +607 -0
  38. custom_widgets_pro/widgets/media/QCustomVideoPlayer.py +326 -0
  39. custom_widgets_pro/widgets/media/__init__.py +1 -0
  40. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/METADATA +112 -0
  41. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/RECORD +45 -0
  42. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/WHEEL +5 -0
  43. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/entry_points.txt +2 -0
  44. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/licenses/LICENSE +91 -0
  45. qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,607 @@
1
+ ########################################################################
2
+ ## SPINN DESIGN CODE
3
+ # YOUTUBE: (SPINN TV) https://www.youtube.com/@SpinnTV
4
+ # WEBSITE: customwidgets.org
5
+ ########################################################################
6
+
7
+ ########################################################################
8
+ ## QCustomMediaTimeline - a horizontal multi-track clip / scrubber timeline.
9
+ ##
10
+ ## The bottom-of-a-video/animation-tool surface: a time RULER with labelled
11
+ ## ticks (0:01 .. 0:09), a draggable PLAYHEAD, and one or more TRACK lanes.
12
+ ## A track is either a lane of rounded CLIP blocks (move the body, trim from
13
+ ## either edge) or a WAVEFORM lane (an audio waveform painted from sample
14
+ ## values). Everything is painted with QPainter so it stays crisp at any size
15
+ ## and every colour is a qproperty that tracks the theme.
16
+ ##
17
+ ## Drag on the ruler (or the playhead) to scrub -> positionChanged(seconds).
18
+ ## Drag a clip body -> clipMoved(track, clip); drag a clip edge -> clipTrimmed.
19
+ ## Build it declaratively: setDuration(), addTrack(), addClip()/setWaveform(),
20
+ ## or setTimeline({...}).
21
+ ########################################################################
22
+ import math
23
+
24
+ from qtpy.QtCore import Qt, Property, QRectF, QPointF, QSize, Signal, QTimer
25
+ from qtpy.QtGui import QColor, QPainter, QBrush, QPen, QFont, QPainterPath, QLinearGradient
26
+ from qtpy.QtWidgets import QWidget, QSizePolicy
27
+
28
+
29
+ class _Clip:
30
+ __slots__ = ("start", "end", "color", "label")
31
+
32
+ def __init__(self, start, end, color=None, label=""):
33
+ self.start = float(start)
34
+ self.end = float(end)
35
+ self.color = QColor(color) if color else None
36
+ self.label = label or ""
37
+
38
+
39
+ class _Track:
40
+ __slots__ = ("name", "kind", "clips", "values", "color")
41
+
42
+ def __init__(self, name="Track", kind="clips", color=None):
43
+ self.name = name
44
+ self.kind = kind # "clips" | "wave"
45
+ self.clips = [] # list[_Clip]
46
+ self.values = [] # waveform samples (any range, auto-normalised)
47
+ self.color = QColor(color) if color else None
48
+
49
+
50
+ class QCustomMediaTimeline(QWidget):
51
+
52
+ WIDGET_ICON = "components/icons/video_settings.png"
53
+ WIDGET_TOOLTIP = "A horizontal multi-track clip / scrubber timeline (ruler, playhead, clips, waveform)"
54
+ WIDGET_MODULE = "Custom_Widgets.QCustomMediaTimeline"
55
+ WIDGET_DOM_XML = """
56
+ <ui language='c++'>
57
+ <widget class='QCustomMediaTimeline' name='customMediaTimeline'>
58
+ <property name='geometry'><rect><x>0</x><y>0</y><width>620</width><height>150</height></rect></property>
59
+ </widget>
60
+ </ui>
61
+ """
62
+ __catalog__ = {
63
+ "name": "QCustomMediaTimeline",
64
+ "props": {"duration": {"type": "double", "default": 10.0},
65
+ "position": {"type": "double", "default": 1.0},
66
+ "bgColor": {"type": "color", "default": "#12141c"},
67
+ "rulerColor": {"type": "color", "default": "#8b90a6"},
68
+ "playheadColor": {"type": "color", "default": "#e7e9f3"},
69
+ "trackBgColor": {"type": "color", "default": "#1b1e2a"},
70
+ "clipColor": {"type": "color", "default": "#c17ce0"},
71
+ "waveColor": {"type": "color", "default": "#8b90a6"},
72
+ "textColor": {"type": "color", "default": "#e7e9f3"},
73
+ "cornerRadius": {"type": "int", "default": 10},
74
+ "playing": {"type": "bool", "default": False},
75
+ "loop": {"type": "bool", "default": True},
76
+ "animated": {"type": "bool", "default": True}},
77
+ "signals": ["positionChanged", "clipMoved", "clipTrimmed", "clipClicked"],
78
+ # Intentionally no tokens_used: this widget resolves no token roles.
79
+ }
80
+ # Every configurable property is exposed to Qt Designer.
81
+ DESIGNER_CUSTOM_PROPS = [
82
+ {"name": "duration", "kind": "double", "group": "Timeline"},
83
+ {"name": "position", "kind": "double", "group": "Timeline"},
84
+ {"name": "cornerRadius", "kind": "int", "group": "Timeline"},
85
+ {"name": "playing", "kind": "bool", "group": "Timeline"},
86
+ {"name": "loop", "kind": "bool", "group": "Timeline"},
87
+ {"name": "animated", "kind": "bool", "group": "Timeline"},
88
+ {"name": "bgColor", "kind": "color", "group": "Colours"},
89
+ {"name": "rulerColor", "kind": "color", "group": "Colours"},
90
+ {"name": "playheadColor", "kind": "color", "group": "Colours"},
91
+ {"name": "trackBgColor", "kind": "color", "group": "Colours"},
92
+ {"name": "clipColor", "kind": "color", "group": "Colours"},
93
+ {"name": "waveColor", "kind": "color", "group": "Colours"},
94
+ {"name": "textColor", "kind": "color", "group": "Colours"},
95
+ ]
96
+
97
+ positionChanged = Signal(float)
98
+ clipMoved = Signal(int, int)
99
+ clipTrimmed = Signal(int, int)
100
+ clipClicked = Signal(int, int)
101
+ playToggled = Signal(bool)
102
+
103
+ def __init__(self, parent=None):
104
+ super().__init__(parent)
105
+ self.setObjectName("QCustomMediaTimeline")
106
+ self.setMouseTracking(True)
107
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
108
+ self.setMinimumHeight(96)
109
+
110
+ self._duration = 10.0
111
+ self._position = 1.0
112
+ self._tracks = []
113
+
114
+ self._bg = QColor("#12141c")
115
+ self._ruler = QColor("#8b90a6")
116
+ self._playhead = QColor("#e7e9f3")
117
+ self._track_bg = QColor("#1b1e2a")
118
+ self._clip = QColor("#c17ce0")
119
+ self._wave = QColor("#8b90a6")
120
+ self._text = QColor("#e7e9f3")
121
+ self._radius = 10
122
+
123
+ # layout metrics
124
+ self._ruler_h = 26.0
125
+ self._track_gap = 8.0
126
+ self._pad = 14.0
127
+
128
+ # interaction
129
+ self._drag = None # ("scrub",) | ("move", ti, ci, grabOffset) | ("trim", ti, ci, edge)
130
+ self._hover_clip = None # (track, clip) under the cursor
131
+
132
+ # animation: a self-driven playhead so the widget animates on its own
133
+ # (play()/pause() = start/stop; `animated` = enable/disable). Clips also
134
+ # glow on hover.
135
+ self._playing = False
136
+ self._loop = True
137
+ self._animated = True
138
+ self._play_timer = QTimer(self)
139
+ self._play_timer.setInterval(33)
140
+ self._play_timer.timeout.connect(self._advance)
141
+
142
+ # ------------------------------------------------------------------ #
143
+ ## Playback (self-animating playhead)
144
+ # ------------------------------------------------------------------ #
145
+ def setPlaying(self, on):
146
+ on = bool(on) and self._animated # disabled animation can't start
147
+ if on == self._playing:
148
+ return
149
+ self._playing = on
150
+ if on:
151
+ self._play_timer.start()
152
+ else:
153
+ self._play_timer.stop()
154
+ self.playToggled.emit(on)
155
+
156
+ def setAnimated(self, on):
157
+ """Enable/disable animation entirely (stops playback when disabled)."""
158
+ self._animated = bool(on)
159
+ if not self._animated:
160
+ self.setPlaying(False)
161
+
162
+ def play(self):
163
+ self.setPlaying(True)
164
+
165
+ def pause(self):
166
+ self.setPlaying(False)
167
+
168
+ def togglePlay(self):
169
+ self.setPlaying(not self._playing)
170
+
171
+ def isPlaying(self):
172
+ return self._playing
173
+
174
+ def _advance(self):
175
+ step = self._play_timer.interval() / 1000.0
176
+ nxt = self._position + step
177
+ if nxt >= self._duration:
178
+ if self._loop:
179
+ nxt = 0.0
180
+ else:
181
+ nxt = self._duration
182
+ self.setPlaying(False)
183
+ self.setPosition(nxt)
184
+
185
+ # ------------------------------------------------------------------ #
186
+ ## Public data API
187
+ # ------------------------------------------------------------------ #
188
+ def setDuration(self, seconds):
189
+ self._duration = max(0.1, float(seconds))
190
+ self.update()
191
+
192
+ def setPosition(self, seconds):
193
+ s = max(0.0, min(self._duration, float(seconds)))
194
+ if s != self._position:
195
+ self._position = s
196
+ self.positionChanged.emit(s)
197
+ self.update()
198
+
199
+ def positionSeconds(self):
200
+ return self._position
201
+
202
+ def addTrack(self, name="Track", kind="clips", color=None):
203
+ t = _Track(name, kind, color)
204
+ self._tracks.append(t)
205
+ self.update()
206
+ return len(self._tracks) - 1
207
+
208
+ def addClip(self, track, start, end, color=None, label=""):
209
+ if 0 <= track < len(self._tracks):
210
+ self._tracks[track].clips.append(_Clip(start, end, color, label))
211
+ self.update()
212
+
213
+ def setWaveform(self, track, values):
214
+ if 0 <= track < len(self._tracks):
215
+ self._tracks[track].values = [float(v) for v in values]
216
+ self.update()
217
+
218
+ def clear(self):
219
+ self._tracks = []
220
+ self.update()
221
+
222
+ def setTimeline(self, data):
223
+ """data = {"duration":10, "position":1,
224
+ "tracks":[{"name","kind","color",
225
+ "clips":[{start,end,color,label}], "values":[...]}]}."""
226
+ data = data or {}
227
+ self.clear()
228
+ if "duration" in data:
229
+ self._duration = max(0.1, float(data["duration"]))
230
+ for t in data.get("tracks", []):
231
+ ti = self.addTrack(t.get("name", "Track"), t.get("kind", "clips"),
232
+ t.get("color"))
233
+ for c in t.get("clips", []):
234
+ self.addClip(ti, c.get("start", 0), c.get("end", 1),
235
+ c.get("color"), c.get("label", ""))
236
+ if t.get("values"):
237
+ self.setWaveform(ti, t["values"])
238
+ if "position" in data:
239
+ self._position = max(0.0, min(self._duration, float(data["position"])))
240
+ self.update()
241
+
242
+ # ------------------------------------------------------------------ #
243
+ ## Geometry
244
+ # ------------------------------------------------------------------ #
245
+ def _content_rect(self):
246
+ return QRectF(self._pad, 0, max(1.0, self.width() - 2 * self._pad),
247
+ self.height())
248
+
249
+ def _t2x(self, t):
250
+ cr = self._content_rect()
251
+ return cr.x() + (t / self._duration) * cr.width()
252
+
253
+ def _x2t(self, x):
254
+ cr = self._content_rect()
255
+ return max(0.0, min(self._duration,
256
+ (x - cr.x()) / cr.width() * self._duration))
257
+
258
+ def _tracks_top(self):
259
+ return self._ruler_h + 6.0
260
+
261
+ def _track_h(self):
262
+ n = max(1, len(self._tracks))
263
+ avail = self.height() - self._tracks_top() - self._pad
264
+ return max(18.0, (avail - (n - 1) * self._track_gap) / n)
265
+
266
+ def _track_rect(self, i):
267
+ th = self._track_h()
268
+ y = self._tracks_top() + i * (th + self._track_gap)
269
+ cr = self._content_rect()
270
+ return QRectF(cr.x(), y, cr.width(), th)
271
+
272
+ def _clip_rect(self, ti, clip):
273
+ tr = self._track_rect(ti)
274
+ x0 = self._t2x(clip.start)
275
+ x1 = self._t2x(clip.end)
276
+ return QRectF(x0, tr.y(), max(6.0, x1 - x0), tr.height())
277
+
278
+ # ------------------------------------------------------------------ #
279
+ ## Painting
280
+ # ------------------------------------------------------------------ #
281
+ def paintEvent(self, e):
282
+ p = QPainter(self)
283
+ p.setRenderHint(QPainter.Antialiasing, True)
284
+ p.fillRect(self.rect(), self._bg)
285
+ self._paint_ruler(p)
286
+ for i, t in enumerate(self._tracks):
287
+ self._paint_track(p, i, t)
288
+ self._paint_playhead(p)
289
+ p.end()
290
+
291
+ def _fmt_time(self, secs):
292
+ m = int(secs) // 60
293
+ s = int(secs) % 60
294
+ return "%d:%02d" % (m, s)
295
+
296
+ def _paint_ruler(self, p):
297
+ cr = self._content_rect()
298
+ f = QFont(self.font())
299
+ f.setPointSizeF(8.0)
300
+ p.setFont(f)
301
+ # choose a tick step that yields ~1s where it fits, else spread out
302
+ step = 1.0
303
+ while (cr.width() / (self._duration / step)) < 42 and step < self._duration:
304
+ step += 1.0
305
+ p.setPen(QPen(QColor(self._ruler.red(), self._ruler.green(),
306
+ self._ruler.blue(), 120), 1.0))
307
+ t = 0.0
308
+ while t <= self._duration + 1e-6:
309
+ x = self._t2x(t)
310
+ p.setPen(QPen(QColor(self._ruler.red(), self._ruler.green(),
311
+ self._ruler.blue(), 90), 1.0))
312
+ p.drawLine(QPointF(x, self._ruler_h - 6), QPointF(x, self._ruler_h - 1))
313
+ p.setPen(QPen(self._ruler))
314
+ p.drawText(QRectF(x - 20, 2, 40, 14), Qt.AlignCenter, self._fmt_time(t))
315
+ t += step
316
+
317
+ def _paint_track(self, p, ti, track):
318
+ tr = self._track_rect(ti)
319
+ r = float(self._radius)
320
+ # lane background
321
+ p.setPen(Qt.NoPen)
322
+ p.setBrush(self._track_bg)
323
+ p.drawRoundedRect(tr, r, r)
324
+
325
+ if track.kind == "wave":
326
+ self._paint_waveform(p, tr, track)
327
+ else:
328
+ for ci, clip in enumerate(track.clips):
329
+ self._paint_clip(p, ti, ci, clip)
330
+
331
+ def _paint_clip(self, p, ti, ci, clip):
332
+ rect = self._clip_rect(ti, clip)
333
+ r = float(self._radius)
334
+ col = clip.color or self._clip
335
+ hovered = (self._hover_clip == (ti, ci))
336
+ top = col.lighter(140 if hovered else 118)
337
+ grad = QLinearGradient(rect.topLeft(), rect.bottomLeft())
338
+ grad.setColorAt(0.0, top)
339
+ grad.setColorAt(1.0, col.lighter(112) if hovered else col)
340
+ p.setPen(QPen(QColor(255, 255, 255, 150), 1.0) if hovered else Qt.NoPen)
341
+ p.setBrush(QBrush(grad))
342
+ p.drawRoundedRect(rect, r, r)
343
+ # trim handles
344
+ p.setBrush(QColor(255, 255, 255, 150))
345
+ hw = 3.0
346
+ for hx in (rect.left() + 5, rect.right() - 5 - hw):
347
+ p.drawRoundedRect(QRectF(hx, rect.center().y() - 7, hw, 14), 1.5, 1.5)
348
+ # label
349
+ if clip.label and rect.width() > 46:
350
+ f = QFont(self.font())
351
+ f.setPointSizeF(8.0)
352
+ f.setBold(True)
353
+ p.setFont(f)
354
+ p.setPen(QPen(QColor("#ffffff")))
355
+ p.drawText(rect.adjusted(12, 0, -12, 0),
356
+ Qt.AlignVCenter | Qt.AlignLeft, clip.label)
357
+
358
+ def _paint_waveform(self, p, tr, track):
359
+ vals = track.values
360
+ col = track.color or self._wave
361
+ inner = tr.adjusted(10, 6, -10, -6)
362
+ mid = inner.center().y()
363
+ if not vals:
364
+ p.setPen(QPen(QColor(col.red(), col.green(), col.blue(), 120), 1.0))
365
+ p.drawLine(QPointF(inner.left(), mid), QPointF(inner.right(), mid))
366
+ return
367
+ n = len(vals)
368
+ peak = max(1e-6, max(abs(v) for v in vals))
369
+ bw = inner.width() / n
370
+ p.setPen(Qt.NoPen)
371
+ p.setBrush(QColor(col.red(), col.green(), col.blue(), 200))
372
+ for i, v in enumerate(vals):
373
+ h = (abs(v) / peak) * (inner.height() / 2)
374
+ x = inner.left() + i * bw
375
+ bar = QRectF(x, mid - h, max(1.0, bw * 0.6), h * 2)
376
+ p.drawRoundedRect(bar, bar.width() / 2, bar.width() / 2)
377
+
378
+ def _paint_playhead(self, p):
379
+ x = self._t2x(self._position)
380
+ top = 2.0
381
+ p.setPen(QPen(self._playhead, 1.6))
382
+ p.drawLine(QPointF(x, top + 10), QPointF(x, self.height() - self._pad + 4))
383
+ # handle triangle
384
+ p.setPen(Qt.NoPen)
385
+ p.setBrush(self._playhead)
386
+ path = QPainterPath()
387
+ path.moveTo(x - 5, top)
388
+ path.lineTo(x + 5, top)
389
+ path.lineTo(x, top + 8)
390
+ path.closeSubpath()
391
+ p.drawPath(path)
392
+
393
+ # ------------------------------------------------------------------ #
394
+ ## Interaction
395
+ # ------------------------------------------------------------------ #
396
+ def _pos(self, e):
397
+ return QPointF(e.position()) if hasattr(e, "position") else QPointF(e.pos())
398
+
399
+ def mousePressEvent(self, e):
400
+ if e.button() != Qt.LeftButton:
401
+ return super().mousePressEvent(e)
402
+ pt = self._pos(e)
403
+ # ruler / above tracks -> scrub
404
+ if pt.y() <= self._tracks_top():
405
+ self._drag = ("scrub",)
406
+ self.setPosition(self._x2t(pt.x()))
407
+ return
408
+ # clip hit?
409
+ for ti, track in enumerate(self._tracks):
410
+ if track.kind != "clips":
411
+ continue
412
+ for ci, clip in enumerate(track.clips):
413
+ rect = self._clip_rect(ti, clip)
414
+ if not rect.contains(pt):
415
+ continue
416
+ edge = 9.0
417
+ if pt.x() - rect.left() <= edge:
418
+ self._drag = ("trim", ti, ci, "start")
419
+ elif rect.right() - pt.x() <= edge:
420
+ self._drag = ("trim", ti, ci, "end")
421
+ else:
422
+ self._drag = ("move", ti, ci, self._x2t(pt.x()) - clip.start)
423
+ self.clipClicked.emit(ti, ci)
424
+ return
425
+ # empty track area -> scrub too
426
+ self._drag = ("scrub",)
427
+ self.setPosition(self._x2t(pt.x()))
428
+
429
+ def mouseMoveEvent(self, e):
430
+ pt = self._pos(e)
431
+ if self._drag is None:
432
+ # cursor feedback + track the hovered clip (for its glow)
433
+ cur = Qt.ArrowCursor
434
+ hover = None
435
+ if pt.y() <= self._tracks_top():
436
+ cur = Qt.SizeHorCursor
437
+ else:
438
+ for ti, track in enumerate(self._tracks):
439
+ if track.kind != "clips":
440
+ continue
441
+ for ci, clip in enumerate(track.clips):
442
+ rect = self._clip_rect(ti, clip)
443
+ if rect.contains(pt):
444
+ hover = (ti, ci)
445
+ if (pt.x() - rect.left() <= 9.0) or (rect.right() - pt.x() <= 9.0):
446
+ cur = Qt.SizeHorCursor
447
+ else:
448
+ cur = Qt.OpenHandCursor
449
+ if hover != self._hover_clip:
450
+ self._hover_clip = hover
451
+ self.update()
452
+ self.setCursor(cur)
453
+ return super().mouseMoveEvent(e)
454
+
455
+ kind = self._drag[0]
456
+ if kind == "scrub":
457
+ self.setPosition(self._x2t(pt.x()))
458
+ return
459
+ t = self._x2t(pt.x())
460
+ ti = self._drag[1]
461
+ ci = self._drag[2]
462
+ clip = self._tracks[ti].clips[ci]
463
+ if kind == "trim":
464
+ edge = self._drag[3]
465
+ if edge == "start":
466
+ clip.start = max(0.0, min(clip.end - 0.2, t))
467
+ else:
468
+ clip.end = min(self._duration, max(clip.start + 0.2, t))
469
+ self.clipTrimmed.emit(ti, ci)
470
+ self.update()
471
+ elif kind == "move":
472
+ grab = self._drag[3]
473
+ length = clip.end - clip.start
474
+ new_start = max(0.0, min(self._duration - length, t - grab))
475
+ clip.start = new_start
476
+ clip.end = new_start + length
477
+ self.clipMoved.emit(ti, ci)
478
+ self.update()
479
+
480
+ def mouseReleaseEvent(self, e):
481
+ self._drag = None
482
+ self.setCursor(Qt.ArrowCursor)
483
+ super().mouseReleaseEvent(e)
484
+
485
+ def leaveEvent(self, e):
486
+ if self._hover_clip is not None:
487
+ self._hover_clip = None
488
+ self.update()
489
+ super().leaveEvent(e)
490
+
491
+ def sizeHint(self):
492
+ return QSize(620, 150)
493
+
494
+ # ------------------------------------------------------------------ #
495
+ ## Designer properties
496
+ # ------------------------------------------------------------------ #
497
+ @Property(float)
498
+ def duration(self):
499
+ return self._duration
500
+
501
+ @duration.setter
502
+ def duration(self, v):
503
+ self.setDuration(v)
504
+
505
+ @Property(float)
506
+ def position(self): # noqa: F811 - Qt property alongside position() method
507
+ return self._position
508
+
509
+ @position.setter
510
+ def position(self, v):
511
+ self.setPosition(v)
512
+
513
+ @Property(bool)
514
+ def playing(self):
515
+ return self._playing
516
+
517
+ @playing.setter
518
+ def playing(self, v):
519
+ self.setPlaying(v)
520
+
521
+ @Property(bool)
522
+ def loop(self):
523
+ return self._loop
524
+
525
+ @loop.setter
526
+ def loop(self, v):
527
+ self._loop = bool(v)
528
+
529
+ @Property(bool)
530
+ def animated(self):
531
+ return self._animated
532
+
533
+ @animated.setter
534
+ def animated(self, v):
535
+ self.setAnimated(v)
536
+
537
+ @Property(QColor)
538
+ def bgColor(self):
539
+ return self._bg
540
+
541
+ @bgColor.setter
542
+ def bgColor(self, c):
543
+ self._bg = QColor(c)
544
+ self.update()
545
+
546
+ @Property(QColor)
547
+ def rulerColor(self):
548
+ return self._ruler
549
+
550
+ @rulerColor.setter
551
+ def rulerColor(self, c):
552
+ self._ruler = QColor(c)
553
+ self.update()
554
+
555
+ @Property(QColor)
556
+ def playheadColor(self):
557
+ return self._playhead
558
+
559
+ @playheadColor.setter
560
+ def playheadColor(self, c):
561
+ self._playhead = QColor(c)
562
+ self.update()
563
+
564
+ @Property(QColor)
565
+ def trackBgColor(self):
566
+ return self._track_bg
567
+
568
+ @trackBgColor.setter
569
+ def trackBgColor(self, c):
570
+ self._track_bg = QColor(c)
571
+ self.update()
572
+
573
+ @Property(QColor)
574
+ def clipColor(self):
575
+ return self._clip
576
+
577
+ @clipColor.setter
578
+ def clipColor(self, c):
579
+ self._clip = QColor(c)
580
+ self.update()
581
+
582
+ @Property(QColor)
583
+ def waveColor(self):
584
+ return self._wave
585
+
586
+ @waveColor.setter
587
+ def waveColor(self, c):
588
+ self._wave = QColor(c)
589
+ self.update()
590
+
591
+ @Property(QColor)
592
+ def textColor(self):
593
+ return self._text
594
+
595
+ @textColor.setter
596
+ def textColor(self, c):
597
+ self._text = QColor(c)
598
+ self.update()
599
+
600
+ @Property(int)
601
+ def cornerRadius(self):
602
+ return self._radius
603
+
604
+ @cornerRadius.setter
605
+ def cornerRadius(self, v):
606
+ self._radius = max(0, int(v))
607
+ self.update()