feat: overview scrollbar on timeline when zoomed in

Thin 8px scrollbar appears above the ruler when the timeline is zoomed.
Shows a draggable thumb representing the current view window. Click
outside the thumb to jump, drag the thumb to pan. Ruler and track
shift down to make room. Scrollbar hidden when not zoomed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 11:47:37 +02:00
parent 07e3a1223c
commit fae5560e2d
+47 -2
View File
@@ -1672,6 +1672,7 @@ class TimelineWidget(QWidget):
# (index, new_start, new_end, old_start, old_end) # (index, new_start, new_end, old_start, old_end)
scan_region_resized = pyqtSignal(int, float, float, float, float) scan_region_resized = pyqtSignal(int, float, float, float, float)
_SCROLLBAR_H = 8 # pixels reserved for the overview scrollbar
_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
@@ -1701,6 +1702,9 @@ class TimelineWidget(QWidget):
self._pan_active = False self._pan_active = False
self._pan_start_x = 0.0 self._pan_start_x = 0.0
self._pan_start_view = 0.0 self._pan_start_view = 0.0
# Scrollbar drag state
self._sb_drag = False
self._sb_drag_offset = 0.0
# Waveform data (numpy array of 0-1 peak values, or None) # Waveform data (numpy array of 0-1 peak values, or None)
self._waveform = None self._waveform = None
@@ -1880,11 +1884,22 @@ class TimelineWidget(QWidget):
p.setRenderHint(QPainter.RenderHint.Antialiasing, False) p.setRenderHint(QPainter.RenderHint.Antialiasing, False)
try: try:
w, h = self.width(), self.height() w, h = self.width(), self.height()
rh = self._RULER_H zoomed = self._view_span > 0 and self._duration > 0
sb_h = self._SCROLLBAR_H if zoomed else 0
rh = sb_h + self._RULER_H
th = h - rh # track height th = h - rh # track height
# ── scrollbar (overview minimap) ──────────────────────────────
if zoomed:
p.fillRect(0, 0, w, sb_h, QColor(18, 18, 18))
thumb_w = max(12, int(self._view_span / self._duration * w))
thumb_x = int(self._view_start / self._duration * w)
p.fillRect(thumb_x, 1, thumb_w, sb_h - 2, QColor(90, 90, 90))
p.setPen(QPen(QColor(55, 55, 55)))
p.drawLine(0, sb_h - 1, w, sb_h - 1)
# ── backgrounds ────────────────────────────────────────────── # ── backgrounds ──────────────────────────────────────────────
p.fillRect(0, 0, w, rh, QColor(22, 22, 22)) # ruler bg p.fillRect(0, sb_h, w, self._RULER_H, QColor(22, 22, 22)) # ruler bg
p.fillRect(0, rh, w, th, QColor(32, 32, 32)) # track bg p.fillRect(0, rh, w, th, QColor(32, 32, 32)) # track bg
# subtle track lane (slightly raised strip in the middle) # subtle track lane (slightly raised strip in the middle)
@@ -2114,6 +2129,23 @@ class TimelineWidget(QWidget):
def mousePressEvent(self, event): def mousePressEvent(self, event):
x = event.position().x() x = event.position().x()
y = event.position().y()
# Scrollbar drag
if event.button() == Qt.MouseButton.LeftButton and self._view_span > 0 and y < self._SCROLLBAR_H:
w = self.width()
thumb_w = max(12, int(self._view_span / self._duration * w))
thumb_x = int(self._view_start / self._duration * w)
if thumb_x <= x <= thumb_x + thumb_w:
self._sb_drag = True
self._sb_drag_offset = x - thumb_x
else:
center_t = x / w * self._duration - self._view_span / 2
self._view_start = center_t
self._clamp_view()
self._sb_drag = True
self._sb_drag_offset = thumb_w / 2
self.update()
return
# Middle-mouse drag pans the view window. # Middle-mouse drag pans the view window.
if event.button() == Qt.MouseButton.MiddleButton and self._view_span > 0: if event.button() == Qt.MouseButton.MiddleButton and self._view_span > 0:
self._pan_active = True self._pan_active = True
@@ -2138,6 +2170,8 @@ class TimelineWidget(QWidget):
def mouseDoubleClickEvent(self, event): def mouseDoubleClickEvent(self, event):
from PyQt6.QtCore import Qt as _Qt from PyQt6.QtCore import Qt as _Qt
if event.button() == _Qt.MouseButton.LeftButton: if event.button() == _Qt.MouseButton.LeftButton:
if self._view_span > 0 and event.position().y() < self._SCROLLBAR_H:
return
x = event.position().x() x = event.position().x()
for (t, _num, output_path, _span) in self._markers: for (t, _num, output_path, _span) in self._markers:
if abs(x - self._time_to_x(t)) <= 10: if abs(x - self._time_to_x(t)) <= 10:
@@ -2153,6 +2187,14 @@ class TimelineWidget(QWidget):
x = event.position().x() x = event.position().x()
w = self.width() w = self.width()
# Active scrollbar drag
if self._sb_drag and event.buttons() & Qt.MouseButton.LeftButton:
new_x = x - self._sb_drag_offset
self._view_start = new_x / max(w, 1) * self._duration
self._clamp_view()
self.update()
return
# Active middle-mouse pan # Active middle-mouse pan
if self._pan_active and event.buttons() & Qt.MouseButton.MiddleButton: if self._pan_active and event.buttons() & Qt.MouseButton.MiddleButton:
dx = x - self._pan_start_x dx = x - self._pan_start_x
@@ -2212,6 +2254,9 @@ class TimelineWidget(QWidget):
self.cursor_changed.emit(self._cursor) self.cursor_changed.emit(self._cursor)
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if self._sb_drag and event.button() == Qt.MouseButton.LeftButton:
self._sb_drag = False
return
if self._pan_active and event.button() == Qt.MouseButton.MiddleButton: if self._pan_active and event.button() == Qt.MouseButton.MiddleButton:
self._pan_active = False self._pan_active = False
self.unsetCursor() self.unsetCursor()