You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

298 lines
12 KiB

/*****************************************************************************
* VLCLibraryAudioGroupTableHeaderView.m: MacOS X interface module
*****************************************************************************
* Copyright (C) 2025 VLC authors and VideoLAN
*
* Authors: Claudio Cambra <developer@claudiocambra.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#import "VLCLibraryAudioGroupTableHeaderView.h"
#import "library/VLCLibraryRepresentedItem.h"
#import "library/VLCLibraryUIUnits.h"
#import "extensions/NSColor+VLCAdditions.h"
#import "extensions/NSFont+VLCAdditions.h"
#import "extensions/NSString+Helpers.h"
#import "extensions/NSView+VLCAdditions.h"
const CGFloat VLCLibraryAudioGroupTableHeaderViewHeight = 86.f;
@interface VLCLibraryAudioGroupTableHeaderView ()
@property NSView *backgroundView;
@property NSStackView *rootStackView;
@property NSStackView *labelsStackView;
@property NSStackView *buttonsStackView;
@property NSTextField *titleField;
@property NSTextField *detailField;
@property NSButton *playButton;
@property NSButton *queueButton;
@property CGFloat backgroundTopInset;
@property CGFloat backgroundBottomInset;
@end
@implementation VLCLibraryAudioGroupTableHeaderView
- (instancetype)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithFrame:(NSRect)frameRect withInternalPaddingAddedForContentView:(BOOL)internalPaddingAddedForContentView
{
self = [super initWithFrame:frameRect];
if (self) {
_internalPaddingAddedForContentView = internalPaddingAddedForContentView;
[self commonInit];
}
return self;
}
- (void)commonInit
{
NSView *contentHostView = self;
self.backgroundTopInset = VLCLibraryUIUnits.largeSpacing + VLCLibraryUIUnits.mediumSpacing;
self.backgroundBottomInset = 0.f;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000
if (@available(macOS 26.0, *)) {
NSGlassEffectView * const glassView = [[NSGlassEffectView alloc] initWithFrame:self.bounds];
glassView.translatesAutoresizingMaskIntoConstraints = NO;
NSView * const glassContentView = [[NSView alloc] initWithFrame:glassView.bounds];
glassContentView.translatesAutoresizingMaskIntoConstraints = NO;
glassView.contentView = glassContentView;
self.backgroundView = glassView;
contentHostView = glassContentView;
self.backgroundBottomInset = VLCLibraryUIUnits.largeSpacing + VLCLibraryUIUnits.mediumSpacing + VLCLibraryUIUnits.smallSpacing;
} else
#endif
if (@available(macOS 10.14, *)) {
NSVisualEffectView * const visualEffectView = [[NSVisualEffectView alloc] initWithFrame:self.bounds];
visualEffectView.translatesAutoresizingMaskIntoConstraints = NO;
visualEffectView.wantsLayer = YES;
visualEffectView.material = NSVisualEffectMaterialPopover;
visualEffectView.blendingMode = NSVisualEffectBlendingModeWithinWindow;
self.backgroundView = visualEffectView;
contentHostView = visualEffectView;
} else {
NSView * const fallbackBackgroundView = [[NSView alloc] initWithFrame:self.bounds];
fallbackBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
fallbackBackgroundView.wantsLayer = YES;
fallbackBackgroundView.layer.backgroundColor = NSColor.windowBackgroundColor.CGColor;
self.backgroundView = fallbackBackgroundView;
contentHostView = fallbackBackgroundView;
}
[self addSubview:self.backgroundView];
self.titleField = [self buildLabelWithFont:NSFont.VLClibrarySectionHeaderFont
textColor:NSColor.labelColor
alignment:NSTextAlignmentLeft];
self.detailField = [self buildLabelWithFont:NSFont.VLCLibrarySubsectionSubheaderFont
textColor:NSColor.secondaryLabelColor
alignment:NSTextAlignmentLeft];
self.playButton = [self buildActionButtonWithTitle:_NS("Play") action:@selector(play:)];
self.queueButton = [self buildActionButtonWithTitle:_NS("Queue") action:@selector(enqueue:)];
NSStackView * const labelsStack = [NSStackView stackViewWithViews:@[self.titleField, self.detailField]];
labelsStack.translatesAutoresizingMaskIntoConstraints = NO;
labelsStack.orientation = NSUserInterfaceLayoutOrientationVertical;
labelsStack.alignment = NSLayoutAttributeLeading;
labelsStack.spacing = VLCLibraryUIUnits.smallSpacing;
[labelsStack setContentHuggingPriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal];
[labelsStack setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
self.labelsStackView = labelsStack;
NSStackView * const buttonsStack = [NSStackView stackViewWithViews:@[self.playButton, self.queueButton]];
buttonsStack.translatesAutoresizingMaskIntoConstraints = NO;
buttonsStack.orientation = NSUserInterfaceLayoutOrientationHorizontal;
buttonsStack.alignment = NSLayoutAttributeCenterY;
buttonsStack.spacing = VLCLibraryUIUnits.smallSpacing;
[buttonsStack setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
[buttonsStack setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal];
self.buttonsStackView = buttonsStack;
[contentHostView addSubview:labelsStack];
[contentHostView addSubview:buttonsStack];
const CGFloat horizontalContentInset = VLCLibraryUIUnits.mediumSpacing;
[NSLayoutConstraint activateConstraints:@[
[labelsStack.leadingAnchor constraintEqualToAnchor:contentHostView.leadingAnchor constant:horizontalContentInset],
[labelsStack.centerYAnchor constraintEqualToAnchor:contentHostView.centerYAnchor],
[contentHostView.trailingAnchor constraintEqualToAnchor:buttonsStack.trailingAnchor constant:horizontalContentInset],
[buttonsStack.centerYAnchor constraintEqualToAnchor:contentHostView.centerYAnchor],
[buttonsStack.leadingAnchor constraintGreaterThanOrEqualToAnchor:labelsStack.trailingAnchor constant:VLCLibraryUIUnits.largeSpacing],
]];
[self updateBackgroundConstraints];
self.layer = [CALayer new];
self.layer.backgroundColor = NSColor.clearColor.CGColor;
if (@available(macOS 26.0, *)) {
} else {
self.backgroundView.layer.borderColor = NSColor.VLCSubtleBorderColor.CGColor;
self.backgroundView.layer.borderWidth = VLCLibraryUIUnits.borderThickness;
self.backgroundView.layer.cornerRadius = VLCLibraryUIUnits.cornerRadius;
self.backgroundView.layer.masksToBounds = YES;
if (@available(macOS 10.14, *)) {
[NSApplication.sharedApplication addObserver:self
forKeyPath:@"effectiveAppearance"
options:NSKeyValueObservingOptionNew
context:nil];
}
}
}
- (void)updateBackgroundConstraints
{
if (!self.internalPaddingAddedForContentView) {
[self.backgroundView applyConstraintsToFillSuperview];
return;
}
[NSLayoutConstraint activateConstraints:@[
[self.backgroundView.topAnchor constraintEqualToAnchor:self.topAnchor constant:self.backgroundTopInset],
[self.backgroundView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[self.backgroundView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[self.backgroundView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-self.backgroundBottomInset],
]];
}
- (void)setInternalPaddingAddedForContentView:(BOOL)internalPaddingAddedForContentView
{
if (_internalPaddingAddedForContentView == internalPaddingAddedForContentView) return;
_internalPaddingAddedForContentView = internalPaddingAddedForContentView;
[self updateBackgroundConstraints];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
if (@available(macOS 26.0, *)) {
return;
} else if ([keyPath isEqualToString:@"effectiveAppearance"]) {
if (@available(macOS 10.14, *)) {
NSAppearance * const appearance = change[NSKeyValueChangeNewKey];
const BOOL isDark = [appearance.name isEqualToString:NSAppearanceNameDarkAqua] ||
[appearance.name isEqualToString:NSAppearanceNameVibrantDark];
self.backgroundView.layer.borderColor = isDark ?
NSColor.VLCDarkSubtleBorderColor.CGColor : NSColor.VLCLightSubtleBorderColor.CGColor;
}
}
}
- (NSTextField *)buildLabelWithFont:(NSFont *)font textColor:(NSColor *)color alignment:(NSTextAlignment)alignment
{
NSTextField *label;
if (@available(macOS 10.12, *)) {
label = [NSTextField labelWithString:@""];
} else {
label = [[NSTextField alloc] initWithFrame:NSZeroRect];
label.editable = NO;
label.bezeled = NO;
label.drawsBackground = NO;
label.selectable = NO;
}
label.font = font;
label.textColor = color;
label.alignment = alignment;
label.lineBreakMode = NSLineBreakByTruncatingTail;
label.translatesAutoresizingMaskIntoConstraints = NO;
return label;
}
- (NSButton *)buildActionButtonWithTitle:(NSString *)title action:(SEL)selector
{
NSButton *button;
if (@available(macOS 10.12, *)) {
button = [NSButton buttonWithTitle:title target:self action:selector];
} else {
button = [[NSButton alloc] initWithFrame:NSZeroRect];
button.title = title;
button.target = self;
button.action = selector;
}
button.bezelStyle = NSBezelStyleRounded;
button.translatesAutoresizingMaskIntoConstraints = NO;
if (@available(macOS 10.14, *))
button.contentTintColor = NSColor.VLCAccentColor;
return button;
}
- (void)setRepresentedItem:(VLCLibraryRepresentedItem *)representedItem
{
_representedItem = representedItem;
[self applyRepresentedItemWithFallbackTitle:nil fallbackDetail:nil];
}
- (void)applyRepresentedItemWithFallbackTitle:(NSString *)fallbackTitle fallbackDetail:(NSString *)fallbackDetail
{
id<VLCMediaLibraryItemProtocol> const item = self.representedItem.item;
if (item == nil) {
self.titleField.stringValue = fallbackTitle ?: @"";
self.detailField.stringValue = fallbackDetail ?: @"";
self.playButton.enabled = NO;
self.queueButton.enabled = NO;
return;
}
self.titleField.stringValue = item.displayString ?: fallbackTitle ?: @"";
self.detailField.stringValue = item.primaryDetailString ?: fallbackDetail ?: @"";
self.playButton.enabled = YES;
self.queueButton.enabled = YES;
}
- (void)updateWithRepresentedItem:(VLCLibraryRepresentedItem *)representedItem
fallbackTitle:(NSString *)fallbackTitle
fallbackDetail:(NSString *)fallbackDetail
{
self.representedItem = representedItem;
[self applyRepresentedItemWithFallbackTitle:fallbackTitle fallbackDetail:fallbackDetail];
}
#pragma mark - Actions
- (void)play:(id)sender
{
[self.representedItem play];
}
- (void)enqueue:(id)sender
{
[self.representedItem queue];
}
@end