feat: improve audio region timeline picking
This commit is contained in:
@@ -1897,7 +1897,7 @@ class TimelineWidget(QWidget):
|
||||
_RULER_H = 22 # pixels reserved for the time ruler
|
||||
_HANDLE_H = 8 # height of the playhead triangle
|
||||
_EDGE_PX = 3 # pixel tolerance for edge hit detection
|
||||
_AUDIO_EDGE_PX = 6 # pixel tolerance for audio-band edge grab
|
||||
_AUDIO_EDGE_PX = 10 # pixel tolerance for audio-band edge grab
|
||||
_AUDIO_MIN_W = 0.05 # seconds — min audio-band width
|
||||
|
||||
def __init__(self):
|
||||
@@ -2109,22 +2109,38 @@ class TimelineWidget(QWidget):
|
||||
self._audio_mode = bool(on)
|
||||
self.update()
|
||||
|
||||
def _audio_hit_at_x(self, x: float) -> str | None:
|
||||
if not self._audio_mode or self._audio_region is None:
|
||||
return None
|
||||
a0, a1 = self._audio_region
|
||||
ax0 = self._time_to_x(a0)
|
||||
ax1 = self._time_to_x(a1)
|
||||
if abs(x - ax0) <= self._AUDIO_EDGE_PX:
|
||||
return "left"
|
||||
if abs(x - ax1) <= self._AUDIO_EDGE_PX:
|
||||
return "right"
|
||||
if ax0 < x < ax1:
|
||||
return "move"
|
||||
return "create"
|
||||
|
||||
def _audio_begin_drag_at_x(self, x: float) -> None:
|
||||
if not self._audio_mode or self._audio_region is None:
|
||||
return
|
||||
hit = self._audio_hit_at_x(x)
|
||||
a0, a1 = self._audio_region
|
||||
ax0 = self._time_to_x(a0); ax1 = self._time_to_x(a1)
|
||||
if abs(x - ax0) <= self._AUDIO_EDGE_PX:
|
||||
self._audio_drag = "left"
|
||||
elif abs(x - ax1) <= self._AUDIO_EDGE_PX:
|
||||
self._audio_drag = "right"
|
||||
elif ax0 < x < ax1:
|
||||
self._audio_drag = "move"
|
||||
self._audio_drag_anchor = self._pos_to_time(int(x)) - a0
|
||||
else:
|
||||
self._audio_drag = None
|
||||
t = max(0.0, min(self._pos_to_time(int(x)), self._duration))
|
||||
self._audio_drag = hit
|
||||
self._audio_drag_orig = self._audio_region if hit is not None else None
|
||||
if hit == "move":
|
||||
self._audio_drag_anchor = t - a0
|
||||
elif hit == "create":
|
||||
self._audio_drag_anchor = t
|
||||
width = max(self._AUDIO_MIN_W, a1 - a0)
|
||||
a0 = max(0.0, min(t, max(0.0, self._duration - width)))
|
||||
self._audio_region = (a0, a0 + width)
|
||||
self.update()
|
||||
if self._audio_drag is not None:
|
||||
self._audio_drag_orig = self._audio_region
|
||||
return
|
||||
|
||||
def _audio_drag_to_x(self, x: float) -> None:
|
||||
if self._audio_drag is None or self._audio_region is None:
|
||||
@@ -2135,10 +2151,19 @@ class TimelineWidget(QWidget):
|
||||
a0 = min(t, a1 - self._AUDIO_MIN_W)
|
||||
elif self._audio_drag == "right":
|
||||
a1 = max(t, a0 + self._AUDIO_MIN_W)
|
||||
else: # move
|
||||
elif self._audio_drag == "move":
|
||||
width = a1 - a0
|
||||
a0 = max(0.0, min(t - self._audio_drag_anchor, self._duration - width))
|
||||
a1 = a0 + width
|
||||
else: # create
|
||||
anchor = self._audio_drag_anchor
|
||||
a0 = max(0.0, min(anchor, t))
|
||||
a1 = min(self._duration, max(anchor, t))
|
||||
if a1 - a0 < self._AUDIO_MIN_W:
|
||||
if a1 + self._AUDIO_MIN_W <= self._duration:
|
||||
a1 = a0 + self._AUDIO_MIN_W
|
||||
else:
|
||||
a0 = max(0.0, a1 - self._AUDIO_MIN_W)
|
||||
self._audio_region = (a0, a1)
|
||||
self.update()
|
||||
|
||||
@@ -2393,6 +2418,8 @@ class TimelineWidget(QWidget):
|
||||
p.setPen(QPen(QColor(0, 220, 190), 2))
|
||||
p.drawLine(ax1, rh, ax1, h)
|
||||
p.drawLine(ax2, rh, ax2, h)
|
||||
p.fillRect(ax1 - 4, rh, 8, th, QColor(0, 220, 190, 120))
|
||||
p.fillRect(ax2 - 4, rh, 8, th, QColor(0, 220, 190, 120))
|
||||
else:
|
||||
p.setPen(QPen(QColor(0, 220, 190), 1, Qt.PenStyle.DashLine))
|
||||
p.drawRect(ax1, rh + 1, aw, th - 2)
|
||||
@@ -2700,6 +2727,16 @@ class TimelineWidget(QWidget):
|
||||
mods = event.modifiers()
|
||||
if (mods & Qt.KeyboardModifier.ShiftModifier) and self._hit_scan_edge(x):
|
||||
self.setCursor(Qt.CursorShape.SizeHorCursor)
|
||||
elif self._audio_mode and self._audio_region is not None:
|
||||
hit = self._audio_hit_at_x(x)
|
||||
if hit in ("left", "right"):
|
||||
self.setCursor(Qt.CursorShape.SizeHorCursor)
|
||||
elif hit == "move":
|
||||
self.setCursor(Qt.CursorShape.OpenHandCursor)
|
||||
elif hit == "create":
|
||||
self.setCursor(Qt.CursorShape.CrossCursor)
|
||||
else:
|
||||
self.unsetCursor()
|
||||
else:
|
||||
self.unsetCursor()
|
||||
|
||||
|
||||
@@ -321,6 +321,56 @@ def test_timeline_audio_band_resize_right(win):
|
||||
tl.set_audio_mode(False)
|
||||
|
||||
|
||||
def test_timeline_audio_click_outside_moves_region_start(win):
|
||||
tl = win._timeline
|
||||
win._file_path = "/x/v.mp4"
|
||||
tl._duration = 20.0
|
||||
tl._view_start = 0.0
|
||||
tl._view_span = 20.0
|
||||
tl.resize(400, tl.height() or 80)
|
||||
tl.set_audio_mode(True)
|
||||
tl.set_audio_region(4.0, 8.0)
|
||||
got = []
|
||||
tl.audio_region_changed.connect(lambda s, e: got.append((s, e)))
|
||||
tl._audio_begin_drag_at_x(tl._time_to_x(12.0))
|
||||
tl._audio_end_drag()
|
||||
assert got[-1] == tl._audio_region
|
||||
s, e = tl._audio_region
|
||||
assert abs(s - 12.0) < 0.1
|
||||
assert abs((e - s) - 4.0) < 0.1
|
||||
|
||||
|
||||
def test_timeline_audio_drag_empty_creates_region(win):
|
||||
tl = win._timeline
|
||||
win._file_path = "/x/v.mp4"
|
||||
tl._duration = 20.0
|
||||
tl._view_start = 0.0
|
||||
tl._view_span = 20.0
|
||||
tl.resize(400, tl.height() or 80)
|
||||
tl.set_audio_mode(True)
|
||||
tl.set_audio_region(4.0, 8.0)
|
||||
tl._audio_begin_drag_at_x(tl._time_to_x(12.0))
|
||||
tl._audio_drag_to_x(tl._time_to_x(15.0))
|
||||
tl._audio_end_drag()
|
||||
s, e = tl._audio_region
|
||||
assert abs(s - 12.0) < 0.1
|
||||
assert abs(e - 15.0) < 0.1
|
||||
|
||||
|
||||
def test_timeline_audio_hover_cursor_state(win):
|
||||
tl = win._timeline
|
||||
tl._duration = 20.0
|
||||
tl._view_start = 0.0
|
||||
tl._view_span = 20.0
|
||||
tl.resize(400, tl.height() or 80)
|
||||
tl.set_audio_mode(True)
|
||||
tl.set_audio_region(4.0, 8.0)
|
||||
assert tl._audio_hit_at_x(tl._time_to_x(4.0)) == "left"
|
||||
assert tl._audio_hit_at_x(tl._time_to_x(8.0)) == "right"
|
||||
assert tl._audio_hit_at_x(tl._time_to_x(6.0)) == "move"
|
||||
assert tl._audio_hit_at_x(tl._time_to_x(12.0)) == "create"
|
||||
|
||||
|
||||
def test_timeline_audio_mode_follows_deck(win):
|
||||
win._control_deck.setCurrentWidget(win._tab_audio)
|
||||
assert win._timeline._audio_mode is True
|
||||
|
||||
Reference in New Issue
Block a user