[vlc-commits] [Git][videolan/vlc][master] macosx/progress bar: add tooltip revealing the position under the cursor

Felix Paul Kühne (@fkuehne) gitlab at videolan.org
Fri May 22 17:46:25 UTC 2026



Felix Paul Kühne pushed to branch master at VideoLAN / VLC


Commits:
802453e1 by Felix Paul Kühne at 2026-05-22T19:28:31+02:00
macosx/progress bar: add tooltip revealing the position under the cursor

This adds a small window floating on top of the slider. Fixes #9499

- - - - -


3 changed files:

- modules/gui/macosx/views/VLCPlaybackProgressSlider.h
- modules/gui/macosx/views/VLCPlaybackProgressSlider.m
- modules/gui/macosx/windows/controlsbar/VLCControlsBarCommon.m


Changes:

=====================================
modules/gui/macosx/views/VLCPlaybackProgressSlider.h
=====================================
@@ -22,6 +22,8 @@
 
 #import <Cocoa/Cocoa.h>
 
+#import <vlc_tick.h>
+
 @interface VLCPlaybackProgressSlider : NSSlider
 
 @property (readwrite, nonatomic) BOOL indefinite;
@@ -30,4 +32,7 @@
 /* Indicates if the slider is scrollable with the mouse or trackpad scrollwheel. */
 @property (readwrite) BOOL scrollable;
 
+/* Duration of the current item, used to compute the time shown on hover. */
+ at property (readwrite, nonatomic) vlc_tick_t mediaDuration;
+
 @end


=====================================
modules/gui/macosx/views/VLCPlaybackProgressSlider.m
=====================================
@@ -22,10 +22,15 @@
 
 #import "VLCPlaybackProgressSlider.h"
 
+#import "extensions/NSString+Helpers.h"
 #import "extensions/NSView+VLCAdditions.h"
 #import "views/VLCPlaybackProgressSliderCell.h"
 
- at implementation VLCPlaybackProgressSlider
+ at implementation VLCPlaybackProgressSlider {
+    NSTrackingArea *_hoverTrackingArea;
+    NSWindow *_hoverWindow;
+    NSTextField *_hoverLabel;
+}
 
 + (Class)cellClass
 {
@@ -129,6 +134,212 @@
     } else {
         [(VLCPlaybackProgressSliderCell*)self.cell setSliderStyleLight];
     }
+    [self applyHoverAppearance];
+}
+
+#pragma mark -
+#pragma mark Hover tracking
+
+- (void)installHoverTrackingAreaIfNeeded
+{
+    if (_hoverTrackingArea != nil) {
+        return;
+    }
+    _hoverTrackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect
+                                                      options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingInVisibleRect | NSTrackingActiveInKeyWindow
+                                                        owner:self
+                                                     userInfo:nil];
+    [self addTrackingArea:_hoverTrackingArea];
+}
+
+- (void)updateTrackingAreas
+{
+    [super updateTrackingAreas];
+    [self installHoverTrackingAreaIfNeeded];
+}
+
+- (void)viewDidMoveToWindow
+{
+    [super viewDidMoveToWindow];
+    if (self.window == nil) {
+        [self hideHoverWindow];
+        return;
+    }
+    [self installHoverTrackingAreaIfNeeded];
+}
+
+- (void)dealloc
+{
+    [self hideHoverWindow];
+}
+
+- (BOOL)hoverIsAvailable
+{
+    return self.enabled && !self.indefinite && self.mediaDuration > 0;
+}
+
+- (void)createHoverWindowIfNeeded
+{
+    if (_hoverWindow != nil) {
+        return;
+    }
+
+    _hoverLabel = [NSTextField labelWithString:@""];
+    _hoverLabel.font =
+        [NSFont monospacedDigitSystemFontOfSize:NSFont.smallSystemFontSize
+                                         weight:NSFontWeightRegular];
+    _hoverLabel.alignment = NSTextAlignmentCenter;
+    _hoverLabel.textColor = NSColor.controlTextColor;
+
+    NSView * const contentView = [[NSView alloc] initWithFrame:NSZeroRect];
+    contentView.wantsLayer = YES;
+    contentView.layer.borderWidth = 0.5;
+    contentView.layer.cornerRadius = 3.0;
+    [contentView addSubview:_hoverLabel];
+
+    _hoverWindow = [[NSPanel alloc] initWithContentRect:NSMakeRect(0, 0, 60, 20)
+                                              styleMask:NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel
+                                                backing:NSBackingStoreBuffered
+                                                  defer:NO];
+    _hoverWindow.contentView = contentView;
+    _hoverWindow.opaque = NO;
+    _hoverWindow.backgroundColor = NSColor.clearColor;
+    _hoverWindow.hasShadow = YES;
+    _hoverWindow.ignoresMouseEvents = YES;
+    _hoverWindow.releasedWhenClosed = NO;
+
+    [self applyHoverAppearance];
+}
+
+- (void)applyHoverAppearance
+{
+    if (_hoverWindow == nil) {
+        return;
+    }
+    NSView * const contentView = _hoverWindow.contentView;
+    if (@available(macOS 11.0, *)) {
+        _hoverWindow.appearance = self.effectiveAppearance;
+        [self.effectiveAppearance performAsCurrentDrawingAppearance:^{
+            contentView.layer.backgroundColor =
+                [NSColor.controlBackgroundColor colorWithAlphaComponent:0.95].CGColor;
+            contentView.layer.borderColor = NSColor.gridColor.CGColor;
+        }];
+        return;
+    }
+    if (@available(macOS 10.14, *)) {
+        _hoverWindow.appearance = self.effectiveAppearance;
+    }
+    contentView.layer.backgroundColor =
+        [NSColor.controlBackgroundColor colorWithAlphaComponent:0.95].CGColor;
+    contentView.layer.borderColor = NSColor.gridColor.CGColor;
+}
+
+- (void)hideHoverWindow
+{
+    if (_hoverWindow == nil) {
+        return;
+    }
+    NSWindow * const parent = _hoverWindow.parentWindow;
+    if (parent != nil) {
+        [parent removeChildWindow:_hoverWindow];
+    }
+    [_hoverWindow orderOut:nil];
+}
+
+- (void)showHoverAtSliderX:(CGFloat)xInSlider time:(vlc_tick_t)time
+{
+    NSWindow * const parentWindow = self.window;
+    if (parentWindow == nil) {
+        return;
+    }
+
+    [self createHoverWindowIfNeeded];
+
+    _hoverLabel.stringValue = [NSString stringWithTimeFromTicks:time];
+    [_hoverLabel sizeToFit];
+
+    const NSSize textSize = _hoverLabel.frame.size;
+    const CGFloat horizontalPadding = 6.0;
+    const CGFloat verticalPadding = 2.0;
+    const CGFloat verticalGap = 6.0;
+    const NSSize windowSize = NSMakeSize(textSize.width + horizontalPadding * 2.0,
+                                         textSize.height + verticalPadding * 2.0);
+    _hoverLabel.frame = NSMakeRect(horizontalPadding, verticalPadding,
+                                   textSize.width, textSize.height);
+
+    const NSRect sliderOnScreen =
+        [parentWindow convertRectToScreen:[self convertRect:self.bounds toView:nil]];
+    const CGFloat anchorScreenX = NSMinX(sliderOnScreen) + xInSlider;
+
+    CGFloat originX = anchorScreenX - windowSize.width / 2.0;
+    originX = MAX(NSMinX(sliderOnScreen),
+                  MIN(originX, NSMaxX(sliderOnScreen) - windowSize.width));
+    const CGFloat originY = NSMaxY(sliderOnScreen) + verticalGap;
+
+    [_hoverWindow setFrame:NSMakeRect(originX, originY,
+                                      windowSize.width, windowSize.height)
+                   display:YES];
+
+    if (_hoverWindow.parentWindow != parentWindow) {
+        [parentWindow addChildWindow:_hoverWindow ordered:NSWindowAbove];
+    } else if (!_hoverWindow.visible) {
+        [_hoverWindow orderFront:nil];
+    }
+}
+
+- (void)reportHoverAtPoint:(NSPoint)locationInSelf
+{
+    if (![self hoverIsAvailable]) {
+        [self hideHoverWindow];
+        return;
+    }
+
+    const NSRect trackRect = [(NSSliderCell *)self.cell barRectFlipped:NO];
+    if (NSWidth(trackRect) <= 0) {
+        [self hideHoverWindow];
+        return;
+    }
+
+    CGFloat fraction = (locationInSelf.x - NSMinX(trackRect)) / NSWidth(trackRect);
+    if (fraction < 0.0) {
+        fraction = 0.0;
+    } else if (fraction > 1.0) {
+        fraction = 1.0;
+    }
+
+    const vlc_tick_t time = (vlc_tick_t)(fraction * self.mediaDuration);
+    [self showHoverAtSliderX:locationInSelf.x time:time];
+}
+
+- (void)mouseEntered:(NSEvent *)event
+{
+    [self reportHoverAtPoint:[self convertPoint:event.locationInWindow fromView:nil]];
+}
+
+- (void)mouseMoved:(NSEvent *)event
+{
+    [self reportHoverAtPoint:[self convertPoint:event.locationInWindow fromView:nil]];
+}
+
+- (void)mouseExited:(NSEvent *)event
+{
+    [self hideHoverWindow];
+}
+
+- (void)mouseDown:(NSEvent *)event
+{
+    // NSSlider runs its own tracking loop here; mouseMoved is not delivered
+    // during the drag, so the hover would otherwise show a stale time.
+    [self hideHoverWindow];
+    [super mouseDown:event];
+}
+
+- (void)setMediaDuration:(vlc_tick_t)mediaDuration
+{
+    _mediaDuration = mediaDuration;
+    if (![self hoverIsAvailable]) {
+        [self hideHoverWindow];
+    }
 }
 
 @end


=====================================
modules/gui/macosx/windows/controlsbar/VLCControlsBarCommon.m
=====================================
@@ -137,9 +137,8 @@
     self.jumpForwardButton.accessibilityLabel = _NS("Jump forwards in current item");
     self.jumpForwardButton.accessibilityTitle = self.jumpForwardButton.toolTip;
 
-    self.timeSlider.toolTip = _NS("Position");
     self.timeSlider.accessibilityLabel = _NS("Playback position");
-    self.timeSlider.accessibilityTitle = self.timeSlider.toolTip;
+    self.timeSlider.accessibilityTitle = _NS("Position");
 
     self.fullscreenButton.toolTip = _NS("Enter fullscreen");
     self.fullscreenButton.accessibilityLabel = self.fullscreenButton.toolTip;
@@ -441,6 +440,7 @@
     self.timeSlider.enabled = duration >= 0 && !buffering && _playerController.seekable;
     self.timeSlider.indefinite = buffering;
     self.timeSlider.floatValue = validInputItem ? _playerController.position : 0.;
+    self.timeSlider.mediaDuration = duration;
 
     [self.timeField setTime:timeString withRemainingTime:remainingTime];
     [self.trailingTimeField setTime:timeString withRemainingTime:remainingTime];



View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/802453e158fa43942a5eb92d34bb84ee524edb63

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/802453e158fa43942a5eb92d34bb84ee524edb63
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list