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
|
_RULER_H = 22 # pixels reserved for the time ruler
|
||||||
_HANDLE_H = 8 # height of the playhead triangle
|
_HANDLE_H = 8 # height of the playhead triangle
|
||||||
_EDGE_PX = 3 # pixel tolerance for edge hit detection
|
_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
|
_AUDIO_MIN_W = 0.05 # seconds — min audio-band width
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -2109,22 +2109,38 @@ class TimelineWidget(QWidget):
|
|||||||
self._audio_mode = bool(on)
|
self._audio_mode = bool(on)
|
||||||
self.update()
|
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:
|
def _audio_begin_drag_at_x(self, x: float) -> None:
|
||||||
if not self._audio_mode or self._audio_region is None:
|
if not self._audio_mode or self._audio_region is None:
|
||||||
return
|
return
|
||||||
|
hit = self._audio_hit_at_x(x)
|
||||||
a0, a1 = self._audio_region
|
a0, a1 = self._audio_region
|
||||||
ax0 = self._time_to_x(a0); ax1 = self._time_to_x(a1)
|
t = max(0.0, min(self._pos_to_time(int(x)), self._duration))
|
||||||
if abs(x - ax0) <= self._AUDIO_EDGE_PX:
|
self._audio_drag = hit
|
||||||
self._audio_drag = "left"
|
self._audio_drag_orig = self._audio_region if hit is not None else None
|
||||||
elif abs(x - ax1) <= self._AUDIO_EDGE_PX:
|
if hit == "move":
|
||||||
self._audio_drag = "right"
|
self._audio_drag_anchor = t - a0
|
||||||
elif ax0 < x < ax1:
|
elif hit == "create":
|
||||||
self._audio_drag = "move"
|
self._audio_drag_anchor = t
|
||||||
self._audio_drag_anchor = self._pos_to_time(int(x)) - a0
|
width = max(self._AUDIO_MIN_W, a1 - a0)
|
||||||
else:
|
a0 = max(0.0, min(t, max(0.0, self._duration - width)))
|
||||||
self._audio_drag = None
|
self._audio_region = (a0, a0 + width)
|
||||||
|
self.update()
|
||||||
if self._audio_drag is not None:
|
if self._audio_drag is not None:
|
||||||
self._audio_drag_orig = self._audio_region
|
return
|
||||||
|
|
||||||
def _audio_drag_to_x(self, x: float) -> None:
|
def _audio_drag_to_x(self, x: float) -> None:
|
||||||
if self._audio_drag is None or self._audio_region is 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)
|
a0 = min(t, a1 - self._AUDIO_MIN_W)
|
||||||
elif self._audio_drag == "right":
|
elif self._audio_drag == "right":
|
||||||
a1 = max(t, a0 + self._AUDIO_MIN_W)
|
a1 = max(t, a0 + self._AUDIO_MIN_W)
|
||||||
else: # move
|
elif self._audio_drag == "move":
|
||||||
width = a1 - a0
|
width = a1 - a0
|
||||||
a0 = max(0.0, min(t - self._audio_drag_anchor, self._duration - width))
|
a0 = max(0.0, min(t - self._audio_drag_anchor, self._duration - width))
|
||||||
a1 = a0 + 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._audio_region = (a0, a1)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@@ -2393,6 +2418,8 @@ class TimelineWidget(QWidget):
|
|||||||
p.setPen(QPen(QColor(0, 220, 190), 2))
|
p.setPen(QPen(QColor(0, 220, 190), 2))
|
||||||
p.drawLine(ax1, rh, ax1, h)
|
p.drawLine(ax1, rh, ax1, h)
|
||||||
p.drawLine(ax2, rh, ax2, 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:
|
else:
|
||||||
p.setPen(QPen(QColor(0, 220, 190), 1, Qt.PenStyle.DashLine))
|
p.setPen(QPen(QColor(0, 220, 190), 1, Qt.PenStyle.DashLine))
|
||||||
p.drawRect(ax1, rh + 1, aw, th - 2)
|
p.drawRect(ax1, rh + 1, aw, th - 2)
|
||||||
@@ -2700,6 +2727,16 @@ class TimelineWidget(QWidget):
|
|||||||
mods = event.modifiers()
|
mods = event.modifiers()
|
||||||
if (mods & Qt.KeyboardModifier.ShiftModifier) and self._hit_scan_edge(x):
|
if (mods & Qt.KeyboardModifier.ShiftModifier) and self._hit_scan_edge(x):
|
||||||
self.setCursor(Qt.CursorShape.SizeHorCursor)
|
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:
|
else:
|
||||||
self.unsetCursor()
|
self.unsetCursor()
|
||||||
|
|
||||||
|
|||||||
@@ -321,6 +321,56 @@ def test_timeline_audio_band_resize_right(win):
|
|||||||
tl.set_audio_mode(False)
|
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):
|
def test_timeline_audio_mode_follows_deck(win):
|
||||||
win._control_deck.setCurrentWidget(win._tab_audio)
|
win._control_deck.setCurrentWidget(win._tab_audio)
|
||||||
assert win._timeline._audio_mode is True
|
assert win._timeline._audio_mode is True
|
||||||
|
|||||||
Reference in New Issue
Block a user