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)
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
_HANDLE_H = 8 # height of the playhead triangle
_EDGE_PX = 3 # pixel tolerance for edge hit detection
@@ -1701,6 +1702,9 @@ class TimelineWidget(QWidget):
self._pan_active = False
self._pan_start_x = 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)
self._waveform = None
@@ -1880,11 +1884,22 @@ class TimelineWidget(QWidget):
p.setRenderHint(QPainter.RenderHint.Antialiasing, False)
try:
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
# ── 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 ──────────────────────────────────────────────
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
# subtle track lane (slightly raised strip in the middle)
@@ -2114,6 +2129,23 @@ class TimelineWidget(QWidget):
def mousePressEvent(self, event):
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.
if event.button() == Qt.MouseButton.MiddleButton and self._view_span > 0:
self._pan_active = True
@@ -2138,6 +2170,8 @@ class TimelineWidget(QWidget):
def mouseDoubleClickEvent(self, event):
from PyQt6.QtCore import Qt as _Qt
if event.button() == _Qt.MouseButton.LeftButton:
if self._view_span > 0 and event.position().y() < self._SCROLLBAR_H:
return
x = event.position().x()
for (t, _num, output_path, _span) in self._markers:
if abs(x - self._time_to_x(t)) <= 10:
@@ -2153,6 +2187,14 @@ class TimelineWidget(QWidget):
x = event.position().x()
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
if self._pan_active and event.buttons() & Qt.MouseButton.MiddleButton:
dx = x - self._pan_start_x
@@ -2212,6 +2254,9 @@ class TimelineWidget(QWidget):
self.cursor_changed.emit(self._cursor)
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:
self._pan_active = False
self.unsetCursor()