[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