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.
- custom_widgets_pro/__init__.py +73 -0
- custom_widgets_pro/_catalog.py +25 -0
- custom_widgets_pro/_cli.py +132 -0
- custom_widgets_pro/_license.py +419 -0
- custom_widgets_pro/_version.py +14 -0
- custom_widgets_pro/datatable/__init__.py +17 -0
- custom_widgets_pro/datatable/datatable_pro.py +319 -0
- custom_widgets_pro/datatable/export.py +143 -0
- custom_widgets_pro/datatable/frozen_view.py +112 -0
- custom_widgets_pro/datatable/grouping.py +309 -0
- custom_widgets_pro/datatable/pivot.py +100 -0
- custom_widgets_pro/datatable/provider.py +98 -0
- custom_widgets_pro/datatable/virtual_model.py +451 -0
- custom_widgets_pro/widgets/__init__.py +7 -0
- custom_widgets_pro/widgets/charts/QCustomBeeswarm.py +317 -0
- custom_widgets_pro/widgets/charts/QCustomBubbleChart.py +655 -0
- custom_widgets_pro/widgets/charts/QCustomCandlestickChart.py +522 -0
- custom_widgets_pro/widgets/charts/QCustomDivergingBarChart.py +430 -0
- custom_widgets_pro/widgets/charts/QCustomDotMatrix.py +266 -0
- custom_widgets_pro/widgets/charts/QCustomFunnelChart.py +474 -0
- custom_widgets_pro/widgets/charts/QCustomGanttChart.py +376 -0
- custom_widgets_pro/widgets/charts/QCustomHeatmap.py +555 -0
- custom_widgets_pro/widgets/charts/QCustomRadarChart.py +600 -0
- custom_widgets_pro/widgets/charts/QCustomRadialBars.py +441 -0
- custom_widgets_pro/widgets/charts/QCustomRadialLines.py +554 -0
- custom_widgets_pro/widgets/charts/QCustomRangeBarChart.py +504 -0
- custom_widgets_pro/widgets/charts/QCustomSankey.py +539 -0
- custom_widgets_pro/widgets/charts/QCustomScatterChart.py +623 -0
- custom_widgets_pro/widgets/charts/__init__.py +1 -0
- custom_widgets_pro/widgets/data/QCustomCodeEditor.py +391 -0
- custom_widgets_pro/widgets/data/QCustomNodeGraph.py +1030 -0
- custom_widgets_pro/widgets/data/QCustomRichTextEditor.py +189 -0
- custom_widgets_pro/widgets/data/QCustomTableToolbar.py +566 -0
- custom_widgets_pro/widgets/data/__init__.py +1 -0
- custom_widgets_pro/widgets/media/QCustomImageViewer.py +300 -0
- custom_widgets_pro/widgets/media/QCustomMediaGrid.py +197 -0
- custom_widgets_pro/widgets/media/QCustomMediaTimeline.py +607 -0
- custom_widgets_pro/widgets/media/QCustomVideoPlayer.py +326 -0
- custom_widgets_pro/widgets/media/__init__.py +1 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/METADATA +112 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/RECORD +45 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/WHEEL +5 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/entry_points.txt +2 -0
- qt_pyqt_pyside_custom_widgets_pro-1.1.0.dist-info/licenses/LICENSE +91 -0
- 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()
|