Added HUD for Tahoe
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
#import <Sparkle/Sparkle.h>
|
#import <Sparkle/Sparkle.h>
|
||||||
|
|
||||||
|
#import "TahoeVolumeHUD.h"
|
||||||
#import "iTunes.h"
|
#import "iTunes.h"
|
||||||
// #import "Music.h"
|
// #import "Music.h"
|
||||||
#import "Spotify.h"
|
#import "Spotify.h"
|
||||||
|
|
@ -18,14 +19,11 @@
|
||||||
|
|
||||||
@class IntroWindowController, AccessibilityDialog, StatusBarItem, PlayerApplication, SystemApplication;
|
@class IntroWindowController, AccessibilityDialog, StatusBarItem, PlayerApplication, SystemApplication;
|
||||||
|
|
||||||
@interface AppDelegate : NSObject <NSApplicationDelegate, NSMenuItemValidation, SPUUpdaterDelegate, SPUStandardUserDriverDelegate> {
|
@interface AppDelegate : NSObject <NSApplicationDelegate, NSMenuItemValidation, SPUUpdaterDelegate, SPUStandardUserDriverDelegate, TahoeVolumeHUDDelegate> {
|
||||||
CALayer *mainLayer;
|
|
||||||
CALayer *volumeImageLayer;
|
CALayer *volumeImageLayer;
|
||||||
CALayer *iconLayer;
|
|
||||||
CALayer *volumeBar[16];
|
CALayer *volumeBar[16];
|
||||||
|
|
||||||
NSImage *imgVolOn,*imgVolOff;
|
NSImage *imgVolOn,*imgVolOff;
|
||||||
NSImage *iTunesIcon,*spotifyIcon;
|
|
||||||
|
|
||||||
NSUserDefaults *preferences;
|
NSUserDefaults *preferences;
|
||||||
|
|
||||||
|
|
@ -122,6 +120,6 @@
|
||||||
@property (assign, nonatomic) double currentVolume;
|
@property (assign, nonatomic) double currentVolume;
|
||||||
@property (assign, nonatomic) double oldVolume;
|
@property (assign, nonatomic) double oldVolume;
|
||||||
@property (assign, nonatomic) double doubleVolume;
|
@property (assign, nonatomic) double doubleVolume;
|
||||||
|
@property (assign, nonatomic) NSImage* icon;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//
|
//
|
||||||
|
//
|
||||||
// AppDelegate.m
|
// AppDelegate.m
|
||||||
// iTunes Volume Control
|
// iTunes Volume Control
|
||||||
//
|
//
|
||||||
|
|
@ -9,6 +10,7 @@
|
||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
#import "SystemVolume.h"
|
#import "SystemVolume.h"
|
||||||
#import "AccessibilityDialog.h"
|
#import "AccessibilityDialog.h"
|
||||||
|
#import "TahoeVolumeHUD.h"
|
||||||
|
|
||||||
#import <IOKit/hidsystem/ev_keymap.h>
|
#import <IOKit/hidsystem/ev_keymap.h>
|
||||||
#import <ServiceManagement/ServiceManagement.h>
|
#import <ServiceManagement/ServiceManagement.h>
|
||||||
|
|
@ -97,7 +99,7 @@ CGEventRef event_tap_callback(CGEventTapProxy proxy, CGEventType type, CGEventRe
|
||||||
keyCode == NX_KEYTYPE_SOUND_UP ||
|
keyCode == NX_KEYTYPE_SOUND_UP ||
|
||||||
keyCode == NX_KEYTYPE_SOUND_DOWN);
|
keyCode == NX_KEYTYPE_SOUND_DOWN);
|
||||||
|
|
||||||
if(isMediaKey) {
|
if(isMediaKey /*&& keyModifier==1114111*/) {
|
||||||
// Hand off all actual logic to main thread
|
// Hand off all actual logic to main thread
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
AppDelegate *app = (__bridge AppDelegate *)refcon;
|
AppDelegate *app = (__bridge AppDelegate *)refcon;
|
||||||
|
|
@ -160,6 +162,7 @@ CGEventRef event_tap_callback(CGEventTapProxy proxy, CGEventType type, CGEventRe
|
||||||
@implementation PlayerApplication
|
@implementation PlayerApplication
|
||||||
|
|
||||||
@synthesize currentVolume = _currentVolume;
|
@synthesize currentVolume = _currentVolume;
|
||||||
|
@synthesize icon = _icon;
|
||||||
|
|
||||||
- (void) setCurrentVolume:(double)currentVolume
|
- (void) setCurrentVolume:(double)currentVolume
|
||||||
{
|
{
|
||||||
|
|
@ -205,12 +208,12 @@ CGEventRef event_tap_callback(CGEventTapProxy proxy, CGEventType type, CGEventRe
|
||||||
return [musicPlayer playerState];
|
return [musicPlayer playerState];
|
||||||
}
|
}
|
||||||
|
|
||||||
-(id)initWithBundleIdentifier:(NSString*) bundleIdentifier {
|
-(id)initWithBundleIdentifier:(NSString*) bundleIdentifier andIcon:(NSImage*)icon {
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
[self setCurrentVolume: -100];
|
[self setCurrentVolume: -100];
|
||||||
[self setOldVolume: -1];
|
[self setOldVolume: -1];
|
||||||
musicPlayer = [SBApplication applicationWithBundleIdentifier:bundleIdentifier];
|
musicPlayer = [SBApplication applicationWithBundleIdentifier:bundleIdentifier];
|
||||||
|
[self setIcon:icon];
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
@ -625,8 +628,10 @@ static NSTimeInterval updateSystemVolumeInterval=0.1f;
|
||||||
|
|
||||||
if(!_hideVolumeWindow){
|
if(!_hideVolumeWindow){
|
||||||
if (@available(macOS 16.0, *)) {
|
if (@available(macOS 16.0, *)) {
|
||||||
// Running on Tahoe (2026) or newer
|
// On Tahoe, show the new popover HUD.
|
||||||
|
[[TahoeVolumeHUD sharedManager] showHUDWithVolume:0 usingMusicPlayer:runningPlayerPtr andLabel:[systemAudio getDefaultOutputDeviceName] anchoredToStatusButton:self.statusBar.button];
|
||||||
} else {
|
} else {
|
||||||
|
// On older systems, use the classic OSD.
|
||||||
id osdMgr = [self->OSDManager sharedManager];
|
id osdMgr = [self->OSDManager sharedManager];
|
||||||
if (osdMgr) {
|
if (osdMgr) {
|
||||||
[osdMgr showImage:OSDGraphicSpeakerMute onDisplayID:CGSMainDisplayID() priority:OSDPriorityDefault msecUntilFade:1000 filledChiclets:0 totalChiclets:(unsigned int)100 locked:NO];
|
[osdMgr showImage:OSDGraphicSpeakerMute onDisplayID:CGSMainDisplayID() priority:OSDPriorityDefault msecUntilFade:1000 filledChiclets:0 totalChiclets:(unsigned int)100 locked:NO];
|
||||||
|
|
@ -641,12 +646,14 @@ static NSTimeInterval updateSystemVolumeInterval=0.1f;
|
||||||
if (_LockSystemAndPlayerVolume && runningPlayerPtr != systemAudio) {
|
if (_LockSystemAndPlayerVolume && runningPlayerPtr != systemAudio) {
|
||||||
[systemAudio setCurrentVolume:[systemAudio oldVolume]];
|
[systemAudio setCurrentVolume:[systemAudio oldVolume]];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!_hideVolumeWindow)
|
if(!_hideVolumeWindow)
|
||||||
{
|
{
|
||||||
if (@available(macOS 16.0, *)) {
|
if (@available(macOS 16.0, *)) {
|
||||||
// Running on Tahoe (2026) or newer
|
// On Tahoe, show the new popover HUD.
|
||||||
|
[[TahoeVolumeHUD sharedManager] showHUDWithVolume:[runningPlayerPtr oldVolume] usingMusicPlayer:runningPlayerPtr andLabel:[systemAudio getDefaultOutputDeviceName] anchoredToStatusButton:self.statusBar.button];
|
||||||
} else {
|
} else {
|
||||||
|
// On older systems, use the classic OSD.
|
||||||
id osdMgr = [self->OSDManager sharedManager];
|
id osdMgr = [self->OSDManager sharedManager];
|
||||||
if (osdMgr) {
|
if (osdMgr) {
|
||||||
[osdMgr showImage:OSDGraphicSpeaker onDisplayID:CGSMainDisplayID() priority:OSDPriorityDefault msecUntilFade:1000 filledChiclets:(unsigned int)[runningPlayerPtr oldVolume] totalChiclets:(unsigned int)100 locked:NO];
|
[osdMgr showImage:OSDGraphicSpeaker onDisplayID:CGSMainDisplayID() priority:OSDPriorityDefault msecUntilFade:1000 filledChiclets:(unsigned int)[runningPlayerPtr oldVolume] totalChiclets:(unsigned int)100 locked:NO];
|
||||||
|
|
@ -733,18 +740,15 @@ static NSTimeInterval updateSystemVolumeInterval=0.1f;
|
||||||
self->OSDManager = NSClassFromString(@"OSDManager");
|
self->OSDManager = NSClassFromString(@"OSDManager");
|
||||||
}
|
}
|
||||||
|
|
||||||
//[self checkSIPforAppIdentifier:@"com.apple.iTunes" promptIfNeeded:YES];
|
if (@available(macOS 16.0, *)) {
|
||||||
//[self checkSIPforAppIdentifier:@"com.spotify.client" promptIfNeeded:YES];
|
iTunes = [[PlayerApplication alloc] initWithBundleIdentifier:@"com.apple.Music" andIcon:[NSImage imageNamed:@"AppleMusicTahoe"]];
|
||||||
|
} else {
|
||||||
|
iTunes = [[PlayerApplication alloc] initWithBundleIdentifier:@"com.apple.iTunes" andIcon:[NSImage imageNamed:@"AppleMusicSequoia"]];
|
||||||
|
}
|
||||||
|
|
||||||
|
spotify = [[PlayerApplication alloc] initWithBundleIdentifier:@"com.spotify.client" andIcon:[NSImage imageNamed:@"spotify"]];
|
||||||
|
|
||||||
if (@available(macOS 10.15, *)) {
|
doppler = [[PlayerApplication alloc] initWithBundleIdentifier:@"co.brushedtype.doppler-macos" andIcon:[NSImage imageNamed:@"doppler"]];
|
||||||
iTunes = [[PlayerApplication alloc] initWithBundleIdentifier:@"com.apple.Music"];
|
|
||||||
} else {
|
|
||||||
iTunes = [[PlayerApplication alloc] initWithBundleIdentifier:@"com.apple.iTunes"];
|
|
||||||
}
|
|
||||||
|
|
||||||
spotify = [[PlayerApplication alloc] initWithBundleIdentifier:@"com.spotify.client"];
|
|
||||||
|
|
||||||
doppler = [[PlayerApplication alloc] initWithBundleIdentifier:@"co.brushedtype.doppler-macos"];
|
|
||||||
|
|
||||||
// Force MacOS to ask for authorization to AppleEvents if this was not already given
|
// Force MacOS to ask for authorization to AppleEvents if this was not already given
|
||||||
if([iTunes isRunning])
|
if([iTunes isRunning])
|
||||||
|
|
@ -807,6 +811,8 @@ static NSTimeInterval updateSystemVolumeInterval=0.1f;
|
||||||
accessibilityDialog = [[AccessibilityDialog alloc] initWithWindowNibName:@"AccessibilityDialog"];
|
accessibilityDialog = [[AccessibilityDialog alloc] initWithWindowNibName:@"AccessibilityDialog"];
|
||||||
[accessibilityDialog showWindow:self];
|
[accessibilityDialog showWindow:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TahoeVolumeHUD sharedManager].delegate = self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag
|
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag
|
||||||
|
|
@ -892,13 +898,7 @@ static NSTimeInterval updateSystemVolumeInterval=0.1f;
|
||||||
[self setLockSystemAndPlayerVolume:[preferences boolForKey: @"LockSystemAndPlayerVolume"]];
|
[self setLockSystemAndPlayerVolume:[preferences boolForKey: @"LockSystemAndPlayerVolume"]];
|
||||||
[self setAutomaticUpdates:[preferences boolForKey: @"AutomaticUpdates"]];
|
[self setAutomaticUpdates:[preferences boolForKey: @"AutomaticUpdates"]];
|
||||||
[self setHideFromStatusBar:[preferences boolForKey: @"hideFromStatusBarPreference"]];
|
[self setHideFromStatusBar:[preferences boolForKey: @"hideFromStatusBarPreference"]];
|
||||||
if (@available(macOS 16.0, *)) {
|
[self setHideVolumeWindow:[preferences boolForKey: @"hideVolumeWindowPreference"]];
|
||||||
// Running on Tahoe (2026) or newer
|
|
||||||
NSMenuItem *item = [self.statusMenu itemWithTag:HIDE_VOLUME_WINDOW_ID];
|
|
||||||
[item setHidden:YES];
|
|
||||||
} else {
|
|
||||||
[self setHideVolumeWindow:[preferences boolForKey: @"hideVolumeWindowPreference"]];
|
|
||||||
}
|
|
||||||
[[self iTunesBtn] setState:[preferences boolForKey: @"iTunesControl"]];
|
[[self iTunesBtn] setState:[preferences boolForKey: @"iTunesControl"]];
|
||||||
if (@available(macOS 10.15, *)) {
|
if (@available(macOS 10.15, *)) {
|
||||||
[[self iTunesBtn] setTitle:@"Music"];
|
[[self iTunesBtn] setTitle:@"Music"];
|
||||||
|
|
@ -1172,7 +1172,7 @@ static NSTimeInterval updateSystemVolumeInterval=0.1f;
|
||||||
NSInteger numQrtsBlks = 0;
|
NSInteger numQrtsBlks = 0;
|
||||||
|
|
||||||
if (@available(macOS 16.0, *)) {
|
if (@available(macOS 16.0, *)) {
|
||||||
// Running on Tahoe (2026) or newer
|
// On Tahoe, show the new popover HUD anchored to the status item.
|
||||||
} else {
|
} else {
|
||||||
image = (volume > 0)? OSDGraphicSpeaker : OSDGraphicSpeakerMute;
|
image = (volume > 0)? OSDGraphicSpeaker : OSDGraphicSpeakerMute;
|
||||||
numFullBlks = floor(volume/6.25);
|
numFullBlks = floor(volume/6.25);
|
||||||
|
|
@ -1181,10 +1181,11 @@ static NSTimeInterval updateSystemVolumeInterval=0.1f;
|
||||||
|
|
||||||
//NSLog(@"%d %d",(int)numFullBlks,(int)numQrtsBlks);
|
//NSLog(@"%d %d",(int)numFullBlks,(int)numQrtsBlks);
|
||||||
|
|
||||||
if(!_hideVolumeWindow)
|
if(!_hideVolumeWindow)
|
||||||
{
|
{
|
||||||
if (@available(macOS 16.0, *)) {
|
if (@available(macOS 16.0, *)) {
|
||||||
// Running on Tahoe (2026) or newer
|
// On Tahoe, show the new popover HUD anchored to the status item.
|
||||||
|
[[TahoeVolumeHUD sharedManager] showHUDWithVolume:volume usingMusicPlayer:runningPlayerPtr andLabel:[systemAudio getDefaultOutputDeviceName] anchoredToStatusButton:self.statusBar.button];
|
||||||
} else {
|
} else {
|
||||||
if(image) {
|
if(image) {
|
||||||
id osdMgr = [self->OSDManager sharedManager];
|
id osdMgr = [self->OSDManager sharedManager];
|
||||||
|
|
@ -1526,6 +1527,48 @@ static NSTimeInterval updateSystemVolumeInterval=0.1f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - TahoeVolumeHUDDelegate
|
||||||
|
|
||||||
|
- (void)hud:(TahoeVolumeHUD *)hud didChangeVolume:(double)volume forPlayer:(PlayerApplication*)controlledPlayer{
|
||||||
|
// This method is called every time the user drags the slider in the HUD.
|
||||||
|
// The received 'volume' is a value between 0.0 and 1.0.
|
||||||
|
|
||||||
|
// 1. Convert the 0.0-1.0 scale to the 0-100 scale our app uses.
|
||||||
|
double volumePercent = volume * 100.0;
|
||||||
|
|
||||||
|
// 2. Get the currently active player, just like we do for the volume keys.
|
||||||
|
id runningPlayerPtr = controlledPlayer;
|
||||||
|
|
||||||
|
if (runningPlayerPtr != nil) {
|
||||||
|
// 3. Set the volume for the active player.
|
||||||
|
[runningPlayerPtr setCurrentVolume:volumePercent];
|
||||||
|
|
||||||
|
// 4. If volume is locked, also set the system volume.
|
||||||
|
if (_LockSystemAndPlayerVolume && runningPlayerPtr != systemAudio) {
|
||||||
|
[systemAudio setCurrentVolume:volumePercent];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Update the percentage labels in the status menu to reflect the change in real-time.
|
||||||
|
if (runningPlayerPtr == iTunes) {
|
||||||
|
[self setItunesVolume:volumePercent];
|
||||||
|
} else if (runningPlayerPtr == spotify) {
|
||||||
|
[self setSpotifyVolume:volumePercent];
|
||||||
|
} else if (runningPlayerPtr == doppler) {
|
||||||
|
[self setDopplerVolume:volumePercent];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_LockSystemAndPlayerVolume || runningPlayerPtr == systemAudio) {
|
||||||
|
[self setSystemVolume:volumePercent];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didChangeVolumeFinal:(TahoeVolumeHUD *)hud {
|
||||||
|
// This is called when the HUD fades out. We can play the feedback sound here
|
||||||
|
[self emitAcousticFeedback];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#pragma mark - Sparkle Delegates
|
#pragma mark - Sparkle Delegates
|
||||||
|
|
||||||
// This is the Objective-C equivalent of the Swift property 'supportsGentleScheduledUpdateReminders'
|
// This is the Objective-C equivalent of the Swift property 'supportsGentleScheduledUpdateReminders'
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
//
|
|
||||||
// ISSoundAdditions.m (ver 1.2 - 2012.10.27)
|
|
||||||
//
|
|
||||||
// Created by Massimo Moiso (2012-09) InerziaSoft
|
|
||||||
// based on an idea of Antonio Nunes, SintraWorks
|
|
||||||
//
|
|
||||||
// Permission is granted free of charge to use this code without restriction
|
|
||||||
// and without limitation, with the only condition that the copyright
|
|
||||||
// notice and this permission shall be included in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
|
|
||||||
/*
|
|
||||||
GOAL
|
|
||||||
This is a category of NSSound build using CoreAudio to get and set the volume of the
|
|
||||||
system sound and some other utility.
|
|
||||||
It was implemented using the Apple documentation and various unattributed code fragments
|
|
||||||
found on the net. For this reason, its use is free for all.
|
|
||||||
|
|
||||||
USE
|
|
||||||
To maintain the Cocoa conventions, a property-like syntax was used; the following
|
|
||||||
methods ("properties") are available:
|
|
||||||
|
|
||||||
(float)systemVolume - return the volume of the default sound device
|
|
||||||
setSystemVolume(float) - set the volume of the default sound device
|
|
||||||
(AudioDeviceID)defaultOutputDevice - return the default output device
|
|
||||||
applyMute(boolean) - enable or disable muting, if supported
|
|
||||||
|
|
||||||
REQUIREMENTS
|
|
||||||
At least MacOS X 10.6
|
|
||||||
Core Audio Framework
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <CoreAudio/CoreAudio.h>
|
|
||||||
#import <AudioToolbox/AudioServices.h>
|
|
||||||
|
|
||||||
@interface NSSound (ISSoundAdditions)
|
|
||||||
|
|
||||||
+ (AudioDeviceID)defaultOutputDevice;
|
|
||||||
|
|
||||||
+ (float)systemVolume;
|
|
||||||
+ (void)setSystemVolume:(float)inVolume;
|
|
||||||
|
|
||||||
+ (void)increaseSystemVolumeBy:(float)amount;
|
|
||||||
+ (void)decreaseSystemVolumeBy:(float)amount;
|
|
||||||
|
|
||||||
+ (void)applyMute:(Boolean)m;
|
|
||||||
+ (Boolean)isMuted;
|
|
||||||
|
|
||||||
#define ZERO_THRESHOLD 0.000 //if the volume should be set under this value, the device will be muted
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface SystemApplication : NSObject{
|
|
||||||
|
|
||||||
NSAppleScript *ASSystemVolume;
|
|
||||||
NSAppleEventDescriptor* AEsetVolume;
|
|
||||||
NSAppleEventDescriptor* AEgetVolume;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@property (assign, nonatomic) double currentVolume; // The sound output volume (0 = minimum, 100 = maximum)
|
|
||||||
@property (assign, nonatomic) double oldVolume;
|
|
||||||
@property (assign, nonatomic) double doubleVolume;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,381 +0,0 @@
|
||||||
//
|
|
||||||
// ISSoundAdditions.m (ver 1.2 - 2012.10.27)
|
|
||||||
//
|
|
||||||
// Created by Massimo Moiso (2012-09) InerziaSoft
|
|
||||||
// based on an idea of Antonio Nunes, SintraWorks
|
|
||||||
//
|
|
||||||
// Permission is granted free of charge to use this code without restriction
|
|
||||||
// and without limitation, with the only condition that the copyright
|
|
||||||
// notice and this permission shall be included in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
#import <Carbon/Carbon.h>
|
|
||||||
|
|
||||||
#import "ISSoundAdditions.h"
|
|
||||||
|
|
||||||
AudioDeviceID obtainDefaultOutputDevice();
|
|
||||||
|
|
||||||
@implementation NSSound (ISSoundAdditions)
|
|
||||||
|
|
||||||
//
|
|
||||||
// Return the ID of the default audio device; this is a C routine
|
|
||||||
//
|
|
||||||
// IN: none
|
|
||||||
// OUT: the ID of the default device or AudioObjectUnknown
|
|
||||||
//
|
|
||||||
AudioDeviceID obtainDefaultOutputDevice()
|
|
||||||
{
|
|
||||||
AudioDeviceID theAnswer = kAudioObjectUnknown;
|
|
||||||
UInt32 theSize = sizeof(AudioDeviceID);
|
|
||||||
AudioObjectPropertyAddress theAddress;
|
|
||||||
|
|
||||||
theAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
|
|
||||||
theAddress.mScope = kAudioObjectPropertyScopeGlobal;
|
|
||||||
theAddress.mElement = kAudioObjectPropertyElementMaster;
|
|
||||||
|
|
||||||
//first be sure that a default device exists
|
|
||||||
if (! AudioObjectHasProperty(kAudioObjectSystemObject, &theAddress) ) {
|
|
||||||
NSLog(@"Unable to get default audio device");
|
|
||||||
return theAnswer;
|
|
||||||
}
|
|
||||||
//get the property 'default output device'
|
|
||||||
OSStatus theError = AudioObjectGetPropertyData(kAudioObjectSystemObject, &theAddress, 0, NULL, &theSize, &theAnswer);
|
|
||||||
if (theError != noErr) {
|
|
||||||
NSLog(@"Unable to get output audio device");
|
|
||||||
return theAnswer;
|
|
||||||
}
|
|
||||||
return theAnswer;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Return the ID of the default audio device; this is a category class method
|
|
||||||
// that can be called from outside
|
|
||||||
//
|
|
||||||
// IN: none
|
|
||||||
// OUT: the ID of the default device or AudioObjectUnknown
|
|
||||||
//
|
|
||||||
+ (AudioDeviceID)defaultOutputDevice
|
|
||||||
{
|
|
||||||
return obtainDefaultOutputDevice();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Return the system sound volume as a float in the range [0...1]
|
|
||||||
//
|
|
||||||
// IN: none
|
|
||||||
// OUT: (float) the volume of the default device
|
|
||||||
//
|
|
||||||
+ (float)systemVolume
|
|
||||||
{
|
|
||||||
AudioDeviceID defaultDevID = kAudioObjectUnknown;
|
|
||||||
UInt32 theSize = sizeof(Float32);
|
|
||||||
OSStatus theError;
|
|
||||||
Float32 theVolume = 0;
|
|
||||||
AudioObjectPropertyAddress theAddress;
|
|
||||||
|
|
||||||
defaultDevID = obtainDefaultOutputDevice();
|
|
||||||
if (defaultDevID == kAudioObjectUnknown) return 0.0; //device not found: return 0
|
|
||||||
|
|
||||||
theAddress.mSelector = kAudioHardwareServiceDeviceProperty_VirtualMasterVolume;
|
|
||||||
theAddress.mScope = kAudioDevicePropertyScopeOutput;
|
|
||||||
theAddress.mElement = kAudioObjectPropertyElementMaster;
|
|
||||||
|
|
||||||
//be sure that the default device has the volume property
|
|
||||||
if (! AudioObjectHasProperty(defaultDevID, &theAddress) ) {
|
|
||||||
NSLog(@"No volume control for device 0x%0x",defaultDevID);
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//now read the property and correct it, if outside [0...1]
|
|
||||||
theError = AudioObjectGetPropertyData(defaultDevID, &theAddress, 0, NULL, &theSize, &theVolume);
|
|
||||||
if ( theError != noErr ) {
|
|
||||||
NSLog(@"Unable to read volume for device 0x%0x", defaultDevID);
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
theVolume = theVolume > 1.0 ? 1.0 : (theVolume < 0.0 ? 0.0 : theVolume);
|
|
||||||
|
|
||||||
return theVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Set the volume of the default device
|
|
||||||
//
|
|
||||||
// IN: (float)the new volume
|
|
||||||
// OUT: none
|
|
||||||
//
|
|
||||||
+ (void)setSystemVolume:(float)theVolume
|
|
||||||
{
|
|
||||||
float newValue = theVolume;
|
|
||||||
AudioObjectPropertyAddress theAddress;
|
|
||||||
AudioDeviceID defaultDevID;
|
|
||||||
OSStatus theError = noErr;
|
|
||||||
UInt32 muted;
|
|
||||||
Boolean canSetVol = YES, muteValue;
|
|
||||||
Boolean hasMute = YES, canMute = YES;
|
|
||||||
|
|
||||||
defaultDevID = obtainDefaultOutputDevice();
|
|
||||||
if (defaultDevID == kAudioObjectUnknown) { //device not found: return without trying to set
|
|
||||||
NSLog(@"Device unknown");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if the new value is in the correct range - normalize it if not
|
|
||||||
newValue = theVolume > 1.0 ? 1.0 : (theVolume <= THRESHOLD ? ZERO_THRESHOLD : theVolume);
|
|
||||||
if (newValue != theVolume) {
|
|
||||||
NSLog(@"Tentative volume (%5.2f) was out of range; reset to %5.2f", theVolume, newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
theAddress.mElement = kAudioObjectPropertyElementMaster;
|
|
||||||
theAddress.mScope = kAudioDevicePropertyScopeOutput;
|
|
||||||
|
|
||||||
//set the selector to mute or not by checking if under threshold and check if a mute command is available
|
|
||||||
if ( (muteValue = (newValue <= ZERO_THRESHOLD)) )
|
|
||||||
{
|
|
||||||
theAddress.mSelector = kAudioDevicePropertyMute;
|
|
||||||
hasMute = AudioObjectHasProperty(defaultDevID, &theAddress);
|
|
||||||
if (hasMute)
|
|
||||||
{
|
|
||||||
theError = AudioObjectIsPropertySettable(defaultDevID, &theAddress, &canMute);
|
|
||||||
if (theError != noErr || !canMute)
|
|
||||||
{
|
|
||||||
canMute = NO;
|
|
||||||
NSLog(@"Should mute device 0x%0x but did not success",defaultDevID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else canMute = NO;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
theAddress.mSelector = kAudioHardwareServiceDeviceProperty_VirtualMasterVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
// **** now manage the volume following the what we found ****
|
|
||||||
|
|
||||||
//be sure the device has a volume command
|
|
||||||
if (! AudioObjectHasProperty(defaultDevID, &theAddress))
|
|
||||||
{
|
|
||||||
NSLog(@"The device 0x%0x does not have a volume to set", defaultDevID);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//be sure the device can set the volume
|
|
||||||
theError = AudioObjectIsPropertySettable(defaultDevID, &theAddress, &canSetVol);
|
|
||||||
if ( theError!=noErr || !canSetVol )
|
|
||||||
{
|
|
||||||
NSLog(@"The volume of device 0x%0x cannot be set", defaultDevID);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//if under the threshold then mute it, only if possible - done/exit
|
|
||||||
if (muteValue && hasMute && canMute)
|
|
||||||
{
|
|
||||||
muted = 1;
|
|
||||||
theError = AudioObjectSetPropertyData(defaultDevID, &theAddress, 0, NULL, sizeof(muted), &muted);
|
|
||||||
if (theError != noErr)
|
|
||||||
{
|
|
||||||
NSLog(@"The device 0x%0x was not muted",defaultDevID);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else //else set it
|
|
||||||
{
|
|
||||||
theError = AudioObjectSetPropertyData(defaultDevID, &theAddress, 0, NULL, sizeof(newValue), &newValue);
|
|
||||||
if (theError != noErr)
|
|
||||||
{
|
|
||||||
NSLog(@"The device 0x%0x was unable to set volume", defaultDevID);
|
|
||||||
}
|
|
||||||
//if device is able to handle muting, maybe it was muted, so unlock it
|
|
||||||
if (hasMute && canMute)
|
|
||||||
{
|
|
||||||
theAddress.mSelector = kAudioDevicePropertyMute;
|
|
||||||
muted = 0;
|
|
||||||
theError = AudioObjectSetPropertyData(defaultDevID, &theAddress, 0, NULL, sizeof(muted), &muted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (theError != noErr) {
|
|
||||||
NSLog(@"Unable to set volume for device 0x%0x", defaultDevID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Increase the volume of the system device by a certain value
|
|
||||||
//
|
|
||||||
// IN: (float) amount of volume to increase
|
|
||||||
// OUT: none
|
|
||||||
//
|
|
||||||
+ (void)increaseSystemVolumeBy:(float)amount {
|
|
||||||
[self setSystemVolume:self.systemVolume+amount];
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Decrease the volume of the system device by a certain value
|
|
||||||
//
|
|
||||||
// IN: (float) amount of volume to decrease
|
|
||||||
// OUT: none
|
|
||||||
//
|
|
||||||
+ (void)decreaseSystemVolumeBy:(float)amount {
|
|
||||||
[self setSystemVolume:self.systemVolume-amount];
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// IN: (Boolean) if true the device is muted, false it is unmated
|
|
||||||
// OUT: none
|
|
||||||
//
|
|
||||||
+ (void)applyMute:(Boolean)m
|
|
||||||
{
|
|
||||||
AudioDeviceID defaultDevID = kAudioObjectUnknown;
|
|
||||||
AudioObjectPropertyAddress theAddress;
|
|
||||||
Boolean hasMute, canMute = YES;
|
|
||||||
OSStatus theError = noErr;
|
|
||||||
UInt32 muted = 0;
|
|
||||||
|
|
||||||
defaultDevID = obtainDefaultOutputDevice();
|
|
||||||
if (defaultDevID == kAudioObjectUnknown) { //device not found
|
|
||||||
NSLog(@"Device unknown");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
theAddress.mElement = kAudioObjectPropertyElementMaster;
|
|
||||||
theAddress.mScope = kAudioDevicePropertyScopeOutput;
|
|
||||||
theAddress.mSelector = kAudioDevicePropertyMute;
|
|
||||||
muted = m ? 1 : 0;
|
|
||||||
|
|
||||||
hasMute = AudioObjectHasProperty(defaultDevID, &theAddress);
|
|
||||||
|
|
||||||
if (hasMute)
|
|
||||||
{
|
|
||||||
theError = AudioObjectIsPropertySettable(defaultDevID, &theAddress, &canMute);
|
|
||||||
if (theError == noErr && canMute)
|
|
||||||
{
|
|
||||||
theError = AudioObjectSetPropertyData(defaultDevID, &theAddress, 0, NULL, sizeof(muted), &muted);
|
|
||||||
if (theError != noErr) NSLog(@"Cannot change mute status of device 0x%0x", defaultDevID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (Boolean)isMuted
|
|
||||||
{
|
|
||||||
AudioDeviceID defaultDevID = kAudioObjectUnknown;
|
|
||||||
AudioObjectPropertyAddress theAddress;
|
|
||||||
Boolean hasMute, canMute = YES;
|
|
||||||
OSStatus theError = noErr;
|
|
||||||
UInt32 muted = 0;
|
|
||||||
UInt32 mutedSize = 4;
|
|
||||||
|
|
||||||
defaultDevID = obtainDefaultOutputDevice();
|
|
||||||
if (defaultDevID == kAudioObjectUnknown) { //device not found
|
|
||||||
NSLog(@"Device unknown");
|
|
||||||
return false; // works, but not the best return code for this
|
|
||||||
}
|
|
||||||
|
|
||||||
theAddress.mElement = kAudioObjectPropertyElementMaster;
|
|
||||||
theAddress.mScope = kAudioDevicePropertyScopeOutput;
|
|
||||||
theAddress.mSelector = kAudioDevicePropertyMute;
|
|
||||||
|
|
||||||
hasMute = AudioObjectHasProperty(defaultDevID, &theAddress);
|
|
||||||
|
|
||||||
if (hasMute)
|
|
||||||
{
|
|
||||||
theError = AudioObjectIsPropertySettable(defaultDevID, &theAddress, &canMute);
|
|
||||||
if (theError == noErr && canMute)
|
|
||||||
{
|
|
||||||
theError = AudioObjectGetPropertyData(defaultDevID, &theAddress, 0, NULL, &mutedSize, &muted);
|
|
||||||
if (muted) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation SystemApplication
|
|
||||||
|
|
||||||
@synthesize currentVolume = _currentVolume;
|
|
||||||
@synthesize doubleVolume = _doubleVolume;
|
|
||||||
|
|
||||||
- (void) setCurrentVolume:(double)currentVolume
|
|
||||||
{
|
|
||||||
[self setDoubleVolume:currentVolume];
|
|
||||||
|
|
||||||
NSAppleEventDescriptor* AEsetVolumeParams = [NSAppleEventDescriptor listDescriptor];
|
|
||||||
[AEsetVolumeParams insertDescriptor:[NSAppleEventDescriptor descriptorWithInt32:(int)currentVolume] atIndex:1];
|
|
||||||
[AEsetVolume setParamDescriptor:AEsetVolumeParams forKeyword:keyDirectObject];
|
|
||||||
|
|
||||||
NSDictionary *error = nil;
|
|
||||||
[ASSystemVolume executeAppleEvent:AEsetVolume error:&error];
|
|
||||||
|
|
||||||
// [NSSound setSystemVolume:currentVolume/100.];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (double) currentVolume
|
|
||||||
{
|
|
||||||
double vol = 0;
|
|
||||||
|
|
||||||
NSAppleEventDescriptor* AEsetVolumeParams = [NSAppleEventDescriptor listDescriptor];
|
|
||||||
[AEsetVolume setParamDescriptor:AEsetVolumeParams forKeyword:keyDirectObject];
|
|
||||||
|
|
||||||
NSDictionary *error = nil;
|
|
||||||
NSAppleEventDescriptor *resultEventDescriptor = [ASSystemVolume executeAppleEvent:AEgetVolume error:&error];
|
|
||||||
if (! resultEventDescriptor) {
|
|
||||||
NSLog(@"%s AppleScript getVolume error = %@", __PRETTY_FUNCTION__, error);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if ([resultEventDescriptor descriptorType] == cLongInteger) {
|
|
||||||
vol = (double)[resultEventDescriptor int32Value];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
NSLog(@"%s AppleScript getVolume error = Return argument has wrong type", __PRETTY_FUNCTION__);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// double vol = (double)[NSSound systemVolume]*100;
|
|
||||||
|
|
||||||
if (fabs(vol-[self doubleVolume])<1)
|
|
||||||
{
|
|
||||||
vol = [self doubleVolume];
|
|
||||||
}
|
|
||||||
|
|
||||||
return vol;
|
|
||||||
}
|
|
||||||
|
|
||||||
-(id)init {
|
|
||||||
if (self = [super init]) {
|
|
||||||
[self setOldVolume: -1];
|
|
||||||
|
|
||||||
NSURL *URL = [[NSBundle mainBundle] URLForResource:@"SystemVolume" withExtension:@"scpt"];
|
|
||||||
if (URL) {
|
|
||||||
ASSystemVolume = [[NSAppleScript alloc] initWithContentsOfURL:URL error:NULL];
|
|
||||||
|
|
||||||
// target
|
|
||||||
ProcessSerialNumber psn = {0, kCurrentProcess};
|
|
||||||
NSAppleEventDescriptor *target = [NSAppleEventDescriptor descriptorWithDescriptorType:typeProcessSerialNumber bytes:&psn length:sizeof(ProcessSerialNumber)];
|
|
||||||
|
|
||||||
// functions
|
|
||||||
NSAppleEventDescriptor *setVolumeHandler = [NSAppleEventDescriptor descriptorWithString:@"setVolume"];
|
|
||||||
NSAppleEventDescriptor *getVolumeHandler = [NSAppleEventDescriptor descriptorWithString:@"getVolume"];
|
|
||||||
|
|
||||||
// events
|
|
||||||
AEsetVolume = [NSAppleEventDescriptor appleEventWithEventClass:kASAppleScriptSuite eventID:kASSubroutineEvent targetDescriptor:target returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID];
|
|
||||||
[AEsetVolume setParamDescriptor:setVolumeHandler forKeyword:keyASSubroutineName];
|
|
||||||
|
|
||||||
AEgetVolume = [NSAppleEventDescriptor appleEventWithEventClass:kASAppleScriptSuite eventID:kASSubroutineEvent targetDescriptor:target returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID];
|
|
||||||
[AEgetVolume setParamDescriptor:getVolumeHandler forKeyword:keyASSubroutineName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
//
|
|
||||||
// IntroWindowController.h
|
|
||||||
// iTunes Volume Control
|
|
||||||
//
|
|
||||||
// Created by Andrea Alberti on 15.12.13.
|
|
||||||
// Copyright (c) 2013 Andrea Alberti. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
|
|
||||||
@class AppDelegate;
|
|
||||||
|
|
||||||
@interface IntroWindowController : NSWindowController
|
|
||||||
{
|
|
||||||
AppDelegate *appDelegate;
|
|
||||||
|
|
||||||
CALayer* iTunesScreenshotIntroLayer;
|
|
||||||
CALayer* statusbarScreenshotIntroLayer;
|
|
||||||
CALayer* arrow_1_Layer;
|
|
||||||
CALayer* arrow_2_Layer;
|
|
||||||
|
|
||||||
IBOutlet NSTextField *iTune_label_1;
|
|
||||||
IBOutlet NSTextField *iTune_label_2;
|
|
||||||
IBOutlet NSTextField *iTune_label_3;
|
|
||||||
|
|
||||||
int step_number;
|
|
||||||
|
|
||||||
@public
|
|
||||||
}
|
|
||||||
|
|
||||||
@property (nonatomic,assign) IBOutlet NSButton *closeButton;
|
|
||||||
@property (nonatomic,assign) IBOutlet NSButton *nextButton;
|
|
||||||
@property (nonatomic,assign) IBOutlet NSButton *previousButton;
|
|
||||||
@property (nonatomic,assign) IBOutlet NSButton *loadIntroAtStartButton;
|
|
||||||
|
|
||||||
- (IBAction)nextButtonClicked:(id)sender;
|
|
||||||
- (IBAction)prevButtonClicked:(id)sender;
|
|
||||||
- (IBAction)closeButtonClicked:(id)sender;
|
|
||||||
- (IBAction)loadIntroAtStartChanged:(id)sender;
|
|
||||||
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
//
|
|
||||||
// IntroWindowController.m
|
|
||||||
// iTunes Volume Control
|
|
||||||
//
|
|
||||||
// Created by Andrea Alberti on 15.12.13.
|
|
||||||
// Copyright (c) 2013 Andrea Alberti. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "IntroWindowController.h"
|
|
||||||
#import <QuartzCore/CoreAnimation.h>
|
|
||||||
#import "AppDelegate.h"
|
|
||||||
|
|
||||||
@interface IntroWindowController ()
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation IntroWindowController
|
|
||||||
|
|
||||||
@synthesize nextButton;
|
|
||||||
@synthesize previousButton;
|
|
||||||
@synthesize closeButton;
|
|
||||||
@synthesize loadIntroAtStartButton;
|
|
||||||
|
|
||||||
- (id)initWithWindow:(NSWindow *)window
|
|
||||||
{
|
|
||||||
self = [super initWithWindow:window];
|
|
||||||
if (self) {
|
|
||||||
// Initialization code here.
|
|
||||||
|
|
||||||
appDelegate = [[NSApplication sharedApplication] delegate];
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:appDelegate selector:@selector(introWindowWillClose:) name:NSWindowWillCloseNotification object:window];
|
|
||||||
|
|
||||||
step_number = 0;
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (IBAction)loadIntroAtStartChanged:(id)sender
|
|
||||||
{
|
|
||||||
[appDelegate setLoadIntroAtStart:[loadIntroAtStartButton state]];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction)closeButtonClicked:(id)sender
|
|
||||||
{
|
|
||||||
[NSTimer scheduledTimerWithTimeInterval:0.2 target:[self window] selector:@selector(close) userInfo:nil repeats:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction)nextButtonClicked:(id)sender
|
|
||||||
{
|
|
||||||
// CABasicAnimation* fadeOutAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
|
|
||||||
// [fadeOutAnimation setDuration:1.f];
|
|
||||||
// [fadeOutAnimation setRemovedOnCompletion:NO];
|
|
||||||
// [fadeOutAnimation setFillMode:kCAFillModeForwards];
|
|
||||||
// [fadeOutAnimation setFromValue:[NSNumber numberWithFloat:1.0f]];
|
|
||||||
// [fadeOutAnimation setToValue:[NSNumber numberWithFloat:0.0f]];
|
|
||||||
|
|
||||||
switch (step_number)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
|
|
||||||
[previousButton setEnabled:YES];
|
|
||||||
|
|
||||||
[[NSAnimationContext currentContext] setDuration:1.0f];
|
|
||||||
[NSAnimationContext beginGrouping];
|
|
||||||
// [introLayer addAnimation:fadeOutAnimation forKey:@"decreaseOpacity"];
|
|
||||||
[[iTune_label_2 animator] setAlphaValue:1.f];
|
|
||||||
[arrow_2_Layer setOpacity:1.f];
|
|
||||||
[NSAnimationContext endGrouping];
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
[nextButton setEnabled:NO];
|
|
||||||
[closeButton setEnabled:YES];
|
|
||||||
|
|
||||||
[[NSAnimationContext currentContext] setDuration:0.8f];
|
|
||||||
[NSAnimationContext beginGrouping];
|
|
||||||
[[iTune_label_1 animator] setAlphaValue:0.f];
|
|
||||||
[[iTune_label_2 animator] setAlphaValue:0.f];
|
|
||||||
[arrow_1_Layer setOpacity:0.f];
|
|
||||||
[arrow_2_Layer setOpacity:0.f];
|
|
||||||
[statusbarScreenshotIntroLayer setOpacity:1.0f];
|
|
||||||
[iTunesScreenshotIntroLayer setOpacity:0.0f];
|
|
||||||
[NSAnimationContext endGrouping];
|
|
||||||
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
step_number++;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction)prevButtonClicked:(id)sender
|
|
||||||
{
|
|
||||||
switch (step_number)
|
|
||||||
{
|
|
||||||
case 1:
|
|
||||||
[previousButton setEnabled:NO];
|
|
||||||
|
|
||||||
[[NSAnimationContext currentContext] setDuration:0.4f];
|
|
||||||
[NSAnimationContext beginGrouping];
|
|
||||||
// [introLayer addAnimation:fadeOutAnimation forKey:@"decreaseOpacity"];
|
|
||||||
[[iTune_label_2 animator] setAlphaValue:0.f];
|
|
||||||
[arrow_2_Layer setOpacity:0.f];
|
|
||||||
[NSAnimationContext endGrouping];
|
|
||||||
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
[nextButton setEnabled:YES];
|
|
||||||
[closeButton setEnabled:NO];
|
|
||||||
|
|
||||||
[[NSAnimationContext currentContext] setDuration:0.4f];
|
|
||||||
[NSAnimationContext beginGrouping];
|
|
||||||
[[iTune_label_1 animator] setAlphaValue:1.f];
|
|
||||||
[[iTune_label_2 animator] setAlphaValue:1.f];
|
|
||||||
[arrow_1_Layer setOpacity:1.f];
|
|
||||||
[arrow_2_Layer setOpacity:1.f];
|
|
||||||
[statusbarScreenshotIntroLayer setOpacity:0.0f];
|
|
||||||
[iTunesScreenshotIntroLayer setOpacity:1.0f];
|
|
||||||
[NSAnimationContext endGrouping];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
step_number--;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
-(void)showFirstMessage:(NSTimer*)theTimer
|
|
||||||
{
|
|
||||||
[[NSAnimationContext currentContext] setDuration:1.3f];
|
|
||||||
[NSAnimationContext beginGrouping];
|
|
||||||
[[iTune_label_1 animator] setAlphaValue:1.f];
|
|
||||||
[arrow_1_Layer setOpacity:1.f];
|
|
||||||
[NSAnimationContext endGrouping];
|
|
||||||
}
|
|
||||||
|
|
||||||
-(void)awakeFromNib
|
|
||||||
{
|
|
||||||
NSWindow* introWindow = [self window];
|
|
||||||
NSView* introContentView = [introWindow contentView];
|
|
||||||
|
|
||||||
CGSize windowViewSize = [introContentView frame].size;
|
|
||||||
|
|
||||||
NSRect imageRect = NSZeroRect;
|
|
||||||
|
|
||||||
NSImage *iTunesScreenshot=[NSImage imageNamed:@"iTunes-screenshot"];
|
|
||||||
NSImage *iTunes_arrow_1=[NSImage imageNamed:@"iTunes-arrow-1"];
|
|
||||||
NSImage *iTunes_arrow_2=[NSImage imageNamed:@"iTunes-arrow-2"];
|
|
||||||
// NSImage *statusbarScreenshot=[NSImage imageNamed:@"statusbar-screenshot"];
|
|
||||||
NSImage *statusbarScreenshot=[NSImage imageNamed:@"keyboard"];
|
|
||||||
|
|
||||||
//[introWindow setContentBorderThickness:36 forEdge:NSMinYEdge];
|
|
||||||
|
|
||||||
CALayer* toplayer = [CALayer layer];
|
|
||||||
//CGColorRef backgroundColor=CGColorCreateGenericRGB(1.f, 1.f, 1.f, 1.f);
|
|
||||||
//[toplayer setBackgroundColor:backgroundColor];
|
|
||||||
//CFRelease(backgroundColor);
|
|
||||||
|
|
||||||
// iTunes screenshot
|
|
||||||
iTunesScreenshotIntroLayer = [CALayer layer];
|
|
||||||
imageRect.size = [iTunesScreenshot size];
|
|
||||||
[iTunesScreenshotIntroLayer setFrame:NSRectToCGRect(imageRect)];
|
|
||||||
[iTunesScreenshotIntroLayer setPosition:CGPointMake(windowViewSize.width/2-16,windowViewSize.height-imageRect.size.height/2-60)];
|
|
||||||
[iTunesScreenshotIntroLayer setContents:iTunesScreenshot];
|
|
||||||
//[introLayer setBorderColor:CGColorCreateGenericRGB(1.f, 0.f, 0.f, 1.f)];
|
|
||||||
//[introLayer setBorderWidth:1];
|
|
||||||
[toplayer addSublayer:iTunesScreenshotIntroLayer];
|
|
||||||
|
|
||||||
// Statusbar screenshot
|
|
||||||
statusbarScreenshotIntroLayer = [CALayer layer];
|
|
||||||
// [statusbarScreenshotIntroLayer setCompositingFilter:[CIFilter filterWithName:@"CIAdditionCompositing"]];
|
|
||||||
|
|
||||||
imageRect.size = [statusbarScreenshot size];
|
|
||||||
[statusbarScreenshotIntroLayer setFrame:NSRectToCGRect(imageRect)];
|
|
||||||
[statusbarScreenshotIntroLayer setPosition:CGPointMake(350,windowViewSize.height-250)];
|
|
||||||
[statusbarScreenshotIntroLayer setContents:statusbarScreenshot];
|
|
||||||
[statusbarScreenshotIntroLayer setOpacity:0.f];
|
|
||||||
[toplayer addSublayer:statusbarScreenshotIntroLayer];
|
|
||||||
|
|
||||||
arrow_1_Layer = [CALayer layer];
|
|
||||||
imageRect.size = [iTunes_arrow_1 size];
|
|
||||||
[arrow_1_Layer setFrame:NSRectToCGRect(imageRect)];
|
|
||||||
[arrow_1_Layer setPosition:CGPointMake(313,windowViewSize.height-94)];
|
|
||||||
[arrow_1_Layer setContents:iTunes_arrow_1];
|
|
||||||
[arrow_1_Layer setOpacity:0.f];
|
|
||||||
[toplayer addSublayer:arrow_1_Layer];
|
|
||||||
|
|
||||||
[iTune_label_1 setAlphaValue:0.f];
|
|
||||||
|
|
||||||
arrow_2_Layer = [CALayer layer];
|
|
||||||
imageRect.size = [iTunes_arrow_2 size];
|
|
||||||
[arrow_2_Layer setFrame:NSRectToCGRect(imageRect)];
|
|
||||||
[arrow_2_Layer setPosition:CGPointMake(240,windowViewSize.height-95)];
|
|
||||||
[arrow_2_Layer setContents:iTunes_arrow_2];
|
|
||||||
[arrow_2_Layer setOpacity:0.f];
|
|
||||||
[toplayer addSublayer:arrow_2_Layer];
|
|
||||||
|
|
||||||
[iTune_label_2 setAlphaValue:0.f];
|
|
||||||
|
|
||||||
NSRect frameRect = NSMakeRect(20,20,40,40); // This will change based on the size you need
|
|
||||||
iTune_label_3 = [[NSTextField alloc] initWithFrame:frameRect];
|
|
||||||
[iTune_label_3 setStringValue:@"My Label"];
|
|
||||||
[iTune_label_3 setBezeled:NO];
|
|
||||||
[iTune_label_3 setDrawsBackground:NO];
|
|
||||||
[iTune_label_3 setEditable:NO];
|
|
||||||
[iTune_label_3 setSelectable:NO];
|
|
||||||
[introContentView addSubview:iTune_label_3];
|
|
||||||
|
|
||||||
[introContentView setLayer:toplayer];
|
|
||||||
[introContentView setWantsLayer:YES];
|
|
||||||
|
|
||||||
NSImage *closeButtonImage=[NSImage imageNamed:@"introButtons-close"];
|
|
||||||
NSImage *closeButtonImageHL=[NSImage imageNamed:@"introButtons-close-HL"];
|
|
||||||
[closeButton setImage: closeButtonImage];
|
|
||||||
[closeButton setAlternateImage: closeButtonImageHL];
|
|
||||||
[closeButton setBordered:NO];
|
|
||||||
[closeButton setEnabled:NO];
|
|
||||||
[[closeButton cell] setHighlightsBy:1];
|
|
||||||
|
|
||||||
NSImage *nextButtonImage=[NSImage imageNamed:@"introButtons-next"];
|
|
||||||
NSImage *nextButtonImageHL=[NSImage imageNamed:@"introButtons-next-HL"];
|
|
||||||
[nextButton setImage: nextButtonImage];
|
|
||||||
[nextButton setAlternateImage: nextButtonImageHL];
|
|
||||||
[nextButton setBordered:NO];
|
|
||||||
[[nextButton cell] setHighlightsBy:1];
|
|
||||||
|
|
||||||
NSImage *prevButtonImage=[NSImage imageNamed:@"introButtons-prev"];
|
|
||||||
NSImage *prevButtonImageHL=[NSImage imageNamed:@"introButtons-prev-HL"];
|
|
||||||
[previousButton setImage: prevButtonImage];
|
|
||||||
[previousButton setAlternateImage: prevButtonImageHL];
|
|
||||||
[previousButton setBordered:NO];
|
|
||||||
[previousButton setEnabled:NO];
|
|
||||||
[[previousButton cell] setHighlightsBy:1];
|
|
||||||
|
|
||||||
[NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(showFirstMessage:) userInfo:nil repeats:NO];
|
|
||||||
|
|
||||||
[loadIntroAtStartButton setState: [appDelegate loadIntroAtStart]];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)windowDidLoad
|
|
||||||
{
|
|
||||||
[super windowDidLoad];
|
|
||||||
|
|
||||||
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -18,8 +18,10 @@
|
||||||
|
|
||||||
-(id)init;
|
-(id)init;
|
||||||
-(bool)isMuted;
|
-(bool)isMuted;
|
||||||
|
-(NSString *)getDefaultOutputDeviceName;
|
||||||
|
|
||||||
@property (assign, nonatomic) double currentVolume; // The sound output volume (0 = minimum, 100 = maximum)
|
@property (assign, nonatomic) double currentVolume; // The sound output volume (0 = minimum, 100 = maximum)
|
||||||
@property (assign, nonatomic) double oldVolume;
|
@property (assign, nonatomic) double oldVolume;
|
||||||
|
@property (assign, nonatomic) NSImage* icon;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
@implementation SystemApplication
|
@implementation SystemApplication
|
||||||
|
|
||||||
@synthesize currentVolume = _currentVolume;
|
@synthesize currentVolume = _currentVolume;
|
||||||
|
@synthesize icon = _icon;
|
||||||
|
|
||||||
-(AudioDeviceID) getDefaultOutputDevice
|
-(AudioDeviceID) getDefaultOutputDevice
|
||||||
{
|
{
|
||||||
|
|
@ -156,6 +157,40 @@
|
||||||
return ((double)volume) * 100.0;
|
return ((double)volume) * 100.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSString *)getDefaultOutputDeviceName
|
||||||
|
{
|
||||||
|
AudioDeviceID defaultOutputDeviceID = [self getDefaultOutputDevice];
|
||||||
|
|
||||||
|
if (defaultOutputDeviceID == kAudioObjectUnknown) {
|
||||||
|
return @"Unknown Device";
|
||||||
|
}
|
||||||
|
|
||||||
|
CFStringRef deviceName = NULL;
|
||||||
|
UInt32 dataSize = sizeof(deviceName);
|
||||||
|
|
||||||
|
AudioObjectPropertyAddress propertyAddress = {
|
||||||
|
kAudioObjectPropertyName,
|
||||||
|
kAudioObjectPropertyScopeGlobal,
|
||||||
|
kAudioObjectPropertyElementMain
|
||||||
|
};
|
||||||
|
|
||||||
|
OSStatus result = AudioObjectGetPropertyData(defaultOutputDeviceID,
|
||||||
|
&propertyAddress,
|
||||||
|
0,
|
||||||
|
NULL,
|
||||||
|
&dataSize,
|
||||||
|
&deviceName);
|
||||||
|
|
||||||
|
if (result != kAudioHardwareNoError || deviceName == NULL) {
|
||||||
|
NSLog(@"Could not get device name for device 0x%0x", defaultOutputDeviceID);
|
||||||
|
return @"Unknown Device";
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *name = [NSString stringWithString:(__bridge NSString *)deviceName];
|
||||||
|
CFRelease(deviceName);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
-(void)dealloc
|
-(void)dealloc
|
||||||
{
|
{
|
||||||
|
|
@ -164,6 +199,11 @@
|
||||||
-(id)init{
|
-(id)init{
|
||||||
if (self = [super init]) {
|
if (self = [super init]) {
|
||||||
[self setOldVolume:[self currentVolume]];
|
[self setOldVolume:[self currentVolume]];
|
||||||
|
if (@available(macOS 16.0, *)) {
|
||||||
|
[self setIcon:[NSImage imageNamed:@"FinderTahoe"]];
|
||||||
|
} else {
|
||||||
|
[self setIcon:[NSImage imageNamed:@"FinderSequoia"]];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24127" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24128" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24127"/>
|
<deployment identifier="macosx"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24128"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
|
|
@ -19,7 +20,7 @@
|
||||||
<window title="Authorization required" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="F0z-JX-Cv5">
|
<window title="Authorization required" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="F0z-JX-Cv5">
|
||||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
<rect key="contentRect" x="1003" y="186" width="771" height="635"/>
|
<rect key="contentRect" x="1003" y="186" width="771" height="635"/>
|
||||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
|
<rect key="screenRect" x="0.0" y="0.0" width="3072" height="1697"/>
|
||||||
<view key="contentView" id="se5-gp-TjO">
|
<view key="contentView" id="se5-gp-TjO">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="771" height="635"/>
|
<rect key="frame" x="0.0" y="0.0" width="771" height="635"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// FILE: HUDPanel.h
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
@interface HUDPanel : NSPanel
|
||||||
|
@end
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// FILE: HUDPanel.m
|
||||||
|
#import "HUDPanel.h"
|
||||||
|
|
||||||
|
@implementation HUDPanel
|
||||||
|
|
||||||
|
// Allow this nonactivating panel to be key so controls (slider) behave nicely.
|
||||||
|
- (BOOL)canBecomeKeyWindow { return YES; }
|
||||||
|
|
||||||
|
// Explicitly state that the panel can become the first responder.
|
||||||
|
// This allows controls within the panel (like the slider) to receive and process mouse events.
|
||||||
|
- (BOOL)acceptsFirstResponder { return YES; }
|
||||||
|
|
||||||
|
// Do not pretend to be the main app window.
|
||||||
|
- (BOOL)canBecomeMainWindow { return NO; }
|
||||||
|
|
||||||
|
// Keep it nonactivating: clicks won’t steal app focus.
|
||||||
|
- (BOOL)worksWhenModal { return YES; }
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
@ -0,0 +1,374 @@
|
||||||
|
//
|
||||||
|
// LiquidGlassView.swift
|
||||||
|
//
|
||||||
|
// Created by Andrea Alberti on 25.10.25.
|
||||||
|
//
|
||||||
|
// A native glass host view (macOS 26+) with graceful fallback,
|
||||||
|
// usable from Objective-C and SwiftUI.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
import QuartzCore
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@objc(LiquidGlassView)
|
||||||
|
public final class LiquidGlassView: NSView {
|
||||||
|
|
||||||
|
// MARK: - Public API (Objective-C visible)
|
||||||
|
|
||||||
|
/// Where your content goes. Replaces any previous content.
|
||||||
|
@objc public var contentView: NSView? {
|
||||||
|
get { contentHost.subviews.first }
|
||||||
|
set {
|
||||||
|
contentHost.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
guard let v = newValue else { return }
|
||||||
|
v.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentHost.addSubview(v)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
v.leadingAnchor.constraint(equalTo: contentHost.leadingAnchor),
|
||||||
|
v.trailingAnchor.constraint(equalTo: contentHost.trailingAnchor),
|
||||||
|
v.topAnchor.constraint(equalTo: contentHost.topAnchor),
|
||||||
|
v.bottomAnchor.constraint(equalTo: contentHost.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Corner radius applied to the glass.
|
||||||
|
@objc public var cornerRadius: CGFloat = 14 {
|
||||||
|
didSet {
|
||||||
|
applyCornerRadius()
|
||||||
|
updateRimPath() // Update rim to match new radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tint color for the glass (native on 26+, fallback paints a faint layer color).
|
||||||
|
@objc public var tintColor: NSColor? {
|
||||||
|
didSet { applyTint() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NSGlassEffectView.Style (0 = Regular, 1 = Clear). Ignored on fallback.
|
||||||
|
@objc public var style: Int = 1 { // default Clear
|
||||||
|
didSet { applyStyle() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a vibrant, glowing white rim around the view.
|
||||||
|
@objc public var hasVibrantRim: Bool = false {
|
||||||
|
didSet {
|
||||||
|
updateRim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience builder
|
||||||
|
@objc public class func glass(withStyle style: Int, cornerRadius: CGFloat, tintColor: NSColor?) -> LiquidGlassView {
|
||||||
|
let v = LiquidGlassView(frame: .zero)
|
||||||
|
v.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
v.style = style
|
||||||
|
v.cornerRadius = cornerRadius
|
||||||
|
v.tintColor = tintColor
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Optional private Tahoe knobs (no-ops if unavailable)
|
||||||
|
|
||||||
|
/// 0..N variants
|
||||||
|
@objc public func setVariantIfAvailable(_ variant: Int) {
|
||||||
|
setPrivate("_variant", value: variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 0/1
|
||||||
|
@objc public func setScrimStateIfAvailable(_ state: Int) {
|
||||||
|
setPrivate("_scrimState", value: state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 0/1
|
||||||
|
@objc public func setSubduedStateIfAvailable(_ state: Int) {
|
||||||
|
setPrivate("_subduedState", value: state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controls whether the glass adapts its appearance to light/dark mode.
|
||||||
|
/// Set to 0 to force a consistent (darker) look.
|
||||||
|
@objc public func setAdaptiveAppearanceIfAvailable(_ state: Int) {
|
||||||
|
setPrivate("_adaptiveAppearance", value: state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accepts a boolean (YES/NO from ObjC) to use a smaller, sharper shadow rim.
|
||||||
|
@objc public func setUseReducedShadowRadiusIfAvailable(_ state: Bool) {
|
||||||
|
setPrivate("_useReducedShadowRadius", value: state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controls a depth-of-field/focus simulation. Usually set to 0 to disable.
|
||||||
|
@objc public func setContentLensingIfAvailable(_ state: Int) {
|
||||||
|
setPrivate("_contentLensing", value: state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SwiftUI-like post-processing (applies CIFilters to the layer).
|
||||||
|
/// Use sparingly; native glass already blurs/tints appropriately.
|
||||||
|
@objc public func applyVisualAdjustments(saturation: CGFloat = 1.0,
|
||||||
|
brightness: CGFloat = 0.0,
|
||||||
|
blur: CGFloat = 0.0) {
|
||||||
|
ensureLayerBacked()
|
||||||
|
var filters: [CIFilter] = []
|
||||||
|
|
||||||
|
if blur > 0 {
|
||||||
|
if let f = CIFilter(name: "CIGaussianBlur") {
|
||||||
|
f.setValue(blur, forKey: kCIInputRadiusKey)
|
||||||
|
filters.append(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if brightness != 0 || saturation != 1.0 {
|
||||||
|
if let f = CIFilter(name: "CIColorControls") {
|
||||||
|
f.setValue(saturation, forKey: "inputSaturation")
|
||||||
|
f.setValue(brightness, forKey: "inputBrightness")
|
||||||
|
filters.append(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backingGlass.layer?.filters = filters.isEmpty ? nil : filters
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Internals
|
||||||
|
|
||||||
|
private let contentHost = NSView(frame: .zero)
|
||||||
|
private var backingGlass: NSView!
|
||||||
|
private var rimLayer: CAShapeLayer?
|
||||||
|
|
||||||
|
public override init(frame frameRect: NSRect) {
|
||||||
|
super.init(frame: frameRect)
|
||||||
|
// The root view must be layer-backed to host the rim layer.
|
||||||
|
wantsLayer = true
|
||||||
|
buildBacking()
|
||||||
|
updateRim()
|
||||||
|
}
|
||||||
|
|
||||||
|
public required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
// The root view must be layer-backed to host the rim layer.
|
||||||
|
wantsLayer = true
|
||||||
|
buildBacking()
|
||||||
|
updateRim()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func layout() {
|
||||||
|
super.layout()
|
||||||
|
// Update the rim's path whenever the view's size changes.
|
||||||
|
updateRimPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func viewDidMoveToSuperview() {
|
||||||
|
super.viewDidMoveToSuperview()
|
||||||
|
guard let s = superview, translatesAutoresizingMaskIntoConstraints == false else { return }
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
leadingAnchor.constraint(equalTo: s.leadingAnchor),
|
||||||
|
trailingAnchor.constraint(equalTo: s.trailingAnchor),
|
||||||
|
topAnchor.constraint(equalTo: s.topAnchor),
|
||||||
|
bottomAnchor.constraint(equalTo: s.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build NSGlassEffectView if available, else NSVisualEffectView
|
||||||
|
private func buildBacking() {
|
||||||
|
let glass: NSView
|
||||||
|
|
||||||
|
if #available(macOS 26.0, *), let GlassType = NSClassFromString("NSGlassEffectView") as? NSView.Type {
|
||||||
|
let g = GlassType.init(frame: bounds)
|
||||||
|
g.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
glass = g
|
||||||
|
} else {
|
||||||
|
let v = NSVisualEffectView(frame: bounds)
|
||||||
|
v.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
v.material = .underWindowBackground
|
||||||
|
v.blendingMode = .behindWindow
|
||||||
|
v.state = .active
|
||||||
|
v.wantsLayer = true
|
||||||
|
v.layer?.masksToBounds = true
|
||||||
|
glass = v
|
||||||
|
}
|
||||||
|
|
||||||
|
backingGlass = glass
|
||||||
|
|
||||||
|
addSubview(backingGlass)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
backingGlass.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
backingGlass.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
backingGlass.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
backingGlass.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
// content host
|
||||||
|
contentHost.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentHost.wantsLayer = false
|
||||||
|
|
||||||
|
if backingGlass.responds(to: NSSelectorFromString("setContentView:")) {
|
||||||
|
backingGlass.setValue(contentHost, forKey: "contentView") // NSGlassEffectView path
|
||||||
|
} else {
|
||||||
|
backingGlass.addSubview(contentHost) // fallback path
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
contentHost.leadingAnchor.constraint(equalTo: backingGlass.leadingAnchor),
|
||||||
|
contentHost.trailingAnchor.constraint(equalTo: backingGlass.trailingAnchor),
|
||||||
|
contentHost.topAnchor.constraint(equalTo: backingGlass.topAnchor),
|
||||||
|
contentHost.bottomAnchor.constraint(equalTo: backingGlass.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply defaults
|
||||||
|
style = 1
|
||||||
|
cornerRadius = 14
|
||||||
|
tintColor = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyStyle() {
|
||||||
|
if backingGlass.responds(to: NSSelectorFromString("setStyle:")) {
|
||||||
|
backingGlass.setValue(style, forKey: "style")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyCornerRadius() {
|
||||||
|
if backingGlass.responds(to: NSSelectorFromString("setCornerRadius:")) {
|
||||||
|
backingGlass.setValue(cornerRadius, forKey: "cornerRadius")
|
||||||
|
} else {
|
||||||
|
ensureLayerBacked()
|
||||||
|
backingGlass.layer?.cornerRadius = cornerRadius
|
||||||
|
backingGlass.layer?.masksToBounds = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyTint() {
|
||||||
|
if backingGlass.responds(to: NSSelectorFromString("setTintColor:")) {
|
||||||
|
backingGlass.setValue(tintColor, forKey: "tintColor")
|
||||||
|
} else if let tint = tintColor {
|
||||||
|
ensureLayerBacked()
|
||||||
|
backingGlass.layer?.backgroundColor = tint.cgColor // very subtle on fallback
|
||||||
|
} else {
|
||||||
|
backingGlass.layer?.backgroundColor = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setPrivate(_ key: String, value: Any) {
|
||||||
|
guard backingGlass.responds(to: NSSelectorFromString("setContentView:")) else { return }
|
||||||
|
// Best-effort, swallow future changes safely
|
||||||
|
guard let glass = backingGlass else { return }
|
||||||
|
(try? ObjcKVC.setValue(value, forKey: key, on: glass)) ?? ()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ensureLayerBacked() {
|
||||||
|
if backingGlass.layer == nil {
|
||||||
|
backingGlass.wantsLayer = true
|
||||||
|
backingGlass.layer = CALayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates, removes, or configures the rim layer based on the `hasVibrantRim` property.
|
||||||
|
private func updateRim() {
|
||||||
|
if hasVibrantRim {
|
||||||
|
guard rimLayer == nil else { return } // Already created
|
||||||
|
|
||||||
|
let newRim = CAShapeLayer()
|
||||||
|
|
||||||
|
newRim.lineWidth = 1
|
||||||
|
newRim.strokeColor = NSColor(white: 1.0, alpha: 0.8).cgColor
|
||||||
|
newRim.fillColor = NSColor.clear.cgColor
|
||||||
|
|
||||||
|
// Configure the "vibrant" glow using the shadow property
|
||||||
|
newRim.shadowColor = NSColor.white.cgColor
|
||||||
|
newRim.shadowRadius = 1.0
|
||||||
|
newRim.shadowOpacity = 0.6
|
||||||
|
newRim.shadowOffset = .zero
|
||||||
|
|
||||||
|
self.layer?.addSublayer(newRim)
|
||||||
|
self.rimLayer = newRim
|
||||||
|
updateRimPath() // Set its initial path
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Remove the rim if it exists
|
||||||
|
rimLayer?.removeFromSuperlayer()
|
||||||
|
rimLayer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recalculates the rim's path to match the view's current bounds and corner radius.
|
||||||
|
private func updateRimPath() {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
guard let rimLayer = rimLayer else { return }
|
||||||
|
|
||||||
|
// Inset the drawing rect by half the line width to keep the stroke centered on the edge
|
||||||
|
let insetRect = bounds.insetBy(dx: rimLayer.lineWidth / 2, dy: rimLayer.lineWidth / 2)
|
||||||
|
let path = NSBezierPath(roundedRect: insetRect, xRadius: cornerRadius, yRadius: cornerRadius)
|
||||||
|
rimLayer.path = path.cgPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tiny helper to avoid bridging crashes if KVC changes in future macOS versions
|
||||||
|
private enum ObjcKVC {
|
||||||
|
static func setValue(_ value: Any, forKey key: String, on object: Any) throws {
|
||||||
|
(object as AnyObject).setValue(value, forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SwiftUI wrapper
|
||||||
|
|
||||||
|
public enum GUI { }
|
||||||
|
|
||||||
|
@available(macOS 26.0, *)
|
||||||
|
public struct CustomGlassEffectView<Content: View>: NSViewRepresentable {
|
||||||
|
private let variant: Int?
|
||||||
|
private let scrimState: Int?
|
||||||
|
private let subduedState: Int?
|
||||||
|
private let style: NSGlassEffectView.Style
|
||||||
|
private let tint: NSColor?
|
||||||
|
private let cornerRadius: CGFloat
|
||||||
|
private let hasVibrantRim: Bool
|
||||||
|
private let content: Content
|
||||||
|
|
||||||
|
public init(variant: Int? = nil,
|
||||||
|
scrimState: Int? = nil,
|
||||||
|
subduedState: Int? = nil,
|
||||||
|
style: NSGlassEffectView.Style = .clear,
|
||||||
|
tint: NSColor? = nil,
|
||||||
|
cornerRadius: CGFloat = 14,
|
||||||
|
hasVibrantRim: Bool = false,
|
||||||
|
@ViewBuilder content: () -> Content)
|
||||||
|
{
|
||||||
|
self.variant = variant
|
||||||
|
self.scrimState = scrimState
|
||||||
|
self.subduedState = subduedState
|
||||||
|
self.style = style
|
||||||
|
self.tint = tint
|
||||||
|
self.cornerRadius = cornerRadius
|
||||||
|
self.hasVibrantRim = hasVibrantRim
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeNSView(context: Context) -> LiquidGlassView {
|
||||||
|
let v = LiquidGlassView.glass(withStyle: Int(style.rawValue),
|
||||||
|
cornerRadius: cornerRadius,
|
||||||
|
tintColor: tint)
|
||||||
|
v.hasVibrantRim = hasVibrantRim
|
||||||
|
|
||||||
|
let hosting = NSHostingView(rootView: content)
|
||||||
|
hosting.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
v.contentView = hosting
|
||||||
|
|
||||||
|
if let variant { v.setVariantIfAvailable(variant) }
|
||||||
|
if let scrimState { v.setScrimStateIfAvailable(scrimState) }
|
||||||
|
if let subduedState { v.setSubduedStateIfAvailable(subduedState) }
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateNSView(_ v: LiquidGlassView, context: Context) {
|
||||||
|
if let hosting = v.contentView as? NSHostingView<Content> {
|
||||||
|
hosting.rootView = content
|
||||||
|
}
|
||||||
|
v.style = Int(style.rawValue)
|
||||||
|
v.cornerRadius = cornerRadius
|
||||||
|
v.tintColor = tint
|
||||||
|
v.hasVibrantRim = hasVibrantRim
|
||||||
|
|
||||||
|
if let variant { v.setVariantIfAvailable(variant) }
|
||||||
|
if let scrimState { v.setScrimStateIfAvailable(scrimState) }
|
||||||
|
if let subduedState { v.setSubduedStateIfAvailable(subduedState) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
//
|
||||||
|
// TahoeVolumeHUD.h
|
||||||
|
//
|
||||||
|
// Created by Andrea Alberti on 25.10.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
// Do not import AppDelegate.h here to avoid circular imports.
|
||||||
|
// Forward-declare the types we only need by pointer.
|
||||||
|
@class TahoeVolumeHUD;
|
||||||
|
@class PlayerApplication;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@protocol TahoeVolumeHUDDelegate <NSObject>
|
||||||
|
@optional
|
||||||
|
/// Called whenever the user changes the slider (0.0–1.0).
|
||||||
|
- (void)hud:(TahoeVolumeHUD *)hud didChangeVolume:(double)volume forPlayer:(PlayerApplication*)controlledPlayer;
|
||||||
|
/// Called whenever the user changes the slider (0.0–1.0) for the last time releasing focus
|
||||||
|
- (void)didChangeVolumeFinal:(TahoeVolumeHUD *)hud;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
/// A singleton, popover-like Tahoe glass HUD anchored to a status bar button.
|
||||||
|
@interface TahoeVolumeHUD : NSObject
|
||||||
|
|
||||||
|
@property (class, readonly, strong) TahoeVolumeHUD *sharedManager;
|
||||||
|
@property (weak, nonatomic, nullable) id<TahoeVolumeHUDDelegate> delegate;
|
||||||
|
|
||||||
|
/// Show/update the HUD under a status bar button. `volume` is 0.0–1.0 (or 0–100; both accepted).
|
||||||
|
- (void)showHUDWithVolume:(double)volume usingMusicPlayer:(PlayerApplication*)controlledPlayer andLabel:(NSString*)label anchoredToStatusButton:(NSStatusBarButton *)button;
|
||||||
|
|
||||||
|
/// Programmatically hide it immediately.
|
||||||
|
- (void)hide;
|
||||||
|
|
||||||
|
/// Optional: update the slider programmatically without showing/hiding.
|
||||||
|
- (void)setVolume:(double)volume;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
@ -0,0 +1,439 @@
|
||||||
|
// FILE: TahoeVolumeHUD.m
|
||||||
|
|
||||||
|
#import "TahoeVolumeHUD.h"
|
||||||
|
#import "VolumeSliderCell.h"
|
||||||
|
#import <AppKit/NSGlassEffectView.h>
|
||||||
|
#import "HUDPanel.h"
|
||||||
|
#import "VolumeSlider.h"
|
||||||
|
#import "AppDelegate.h"
|
||||||
|
#import <QuartzCore/QuartzCore.h>
|
||||||
|
|
||||||
|
// Product Module Name: Volume_Control
|
||||||
|
#import "Volume_Control-Swift.h" // exposes LiquidGlassView to ObjC
|
||||||
|
|
||||||
|
// **IMPROVEMENT 1: Add VolumeSliderDelegate protocol conformance**
|
||||||
|
@interface TahoeVolumeHUD () <VolumeSliderDelegate>
|
||||||
|
|
||||||
|
// Window + layout
|
||||||
|
@property (strong) HUDPanel *panel;
|
||||||
|
@property (strong) NSView *root;
|
||||||
|
@property (strong) LiquidGlassView *glass;
|
||||||
|
|
||||||
|
// UI
|
||||||
|
// **IMPROVEMENT 2: Change property type to be more specific**
|
||||||
|
@property (strong) VolumeSlider *slider;
|
||||||
|
@property (strong) NSImageView *appIconView;
|
||||||
|
@property (strong) NSTimer *hideTimer;
|
||||||
|
@property (strong) NSTextField *titleLabel;
|
||||||
|
|
||||||
|
// Constraints
|
||||||
|
@property (strong) NSLayoutConstraint *contentFixedHeight;
|
||||||
|
|
||||||
|
// Player
|
||||||
|
@property (strong) PlayerApplication *controlledPlayer;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
// Tunables
|
||||||
|
static const CGFloat kHUDHeight = 64.0;
|
||||||
|
static const CGFloat kHUDWidth = 290.0;
|
||||||
|
static const CGFloat kCornerRadius = 24.0;
|
||||||
|
static const CGFloat kBelowGap = 14.0;
|
||||||
|
static const NSTimeInterval kAutoHide = 1.5;
|
||||||
|
static const CGFloat kSideInset = 12.0; // left/right margin
|
||||||
|
|
||||||
|
static const NSTimeInterval kFadeInDuration = 0.25; // seconds
|
||||||
|
static const NSTimeInterval kFadeOutDuration = 0.45; // seconds
|
||||||
|
|
||||||
|
@implementation TahoeVolumeHUD
|
||||||
|
|
||||||
|
+ (TahoeVolumeHUD *)sharedManager {
|
||||||
|
static TahoeVolumeHUD *instance;
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
instance = [[TahoeVolumeHUD alloc] initPrivate];
|
||||||
|
});
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initPrivate {
|
||||||
|
self = [super init];
|
||||||
|
if (!self) return nil;
|
||||||
|
|
||||||
|
// ... (this part is correct) ...
|
||||||
|
// Panel
|
||||||
|
NSRect frame = NSMakeRect(0, 0, kHUDWidth, kHUDHeight);
|
||||||
|
_panel = [[HUDPanel alloc] initWithContentRect:frame
|
||||||
|
styleMask:(NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel)
|
||||||
|
backing:NSBackingStoreBuffered
|
||||||
|
defer:NO];
|
||||||
|
_panel.opaque = NO;
|
||||||
|
_panel.backgroundColor = NSColor.clearColor;
|
||||||
|
_panel.hasShadow = NO;
|
||||||
|
_panel.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark];
|
||||||
|
|
||||||
|
// Set to NO to prevent the panel from hiding when the app is not active.
|
||||||
|
_panel.hidesOnDeactivate = NO;
|
||||||
|
|
||||||
|
_panel.level = NSPopUpMenuWindowLevel;
|
||||||
|
_panel.movableByWindowBackground = NO;
|
||||||
|
|
||||||
|
// Ensure the panel can appear on any space.
|
||||||
|
// This makes it a true system-wide HUD.
|
||||||
|
_panel.collectionBehavior = NSWindowCollectionBehaviorTransient
|
||||||
|
| NSWindowCollectionBehaviorFullScreenAuxiliary
|
||||||
|
| NSWindowCollectionBehaviorCanJoinAllSpaces;
|
||||||
|
|
||||||
|
_panel.floatingPanel = YES;
|
||||||
|
_panel.becomesKeyOnlyIfNeeded = YES;
|
||||||
|
|
||||||
|
// Size fences
|
||||||
|
_panel.contentMinSize = NSMakeSize(kHUDWidth, kHUDHeight);
|
||||||
|
_panel.contentMaxSize = NSMakeSize(FLT_MAX, kHUDHeight);
|
||||||
|
[_panel setContentSize:NSMakeSize(kHUDWidth, kHUDHeight)];
|
||||||
|
|
||||||
|
// Root host
|
||||||
|
_root = [[NSView alloc] initWithFrame:_panel.contentView.bounds];
|
||||||
|
_root.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
_root.wantsLayer = YES;
|
||||||
|
_root.layer.backgroundColor = NSColor.clearColor.CGColor;
|
||||||
|
_panel.contentView = _root;
|
||||||
|
|
||||||
|
// Hard height on the window contentView — guarantees kHUDHeight regardless of fitting sizes
|
||||||
|
self.contentFixedHeight = [_panel.contentView.heightAnchor constraintEqualToConstant:kHUDHeight];
|
||||||
|
self.contentFixedHeight.priority = 1000;
|
||||||
|
self.contentFixedHeight.active = YES;
|
||||||
|
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
[_root.leadingAnchor constraintEqualToAnchor:_panel.contentView.leadingAnchor],
|
||||||
|
[_root.trailingAnchor constraintEqualToAnchor:_panel.contentView.trailingAnchor],
|
||||||
|
[_root.topAnchor constraintEqualToAnchor:_panel.contentView.topAnchor],
|
||||||
|
[_root.bottomAnchor constraintEqualToAnchor:_panel.contentView.bottomAnchor],
|
||||||
|
]];
|
||||||
|
|
||||||
|
// Glass (Swift class)
|
||||||
|
[self installGlassInto:_root cornerRadius:kCornerRadius];
|
||||||
|
|
||||||
|
// Content wrapper (fills the glass)
|
||||||
|
NSView *wrapper = [NSView new];
|
||||||
|
wrapper.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
|
||||||
|
// 1 of 3: Set the appearance for the entire content view.
|
||||||
|
// This forces all subviews (labels, icons) to use their dark variants.
|
||||||
|
wrapper.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark];
|
||||||
|
|
||||||
|
// Build header row (icon + label) and slider strip
|
||||||
|
NSView *header = [self buildHeaderRow];
|
||||||
|
NSView *strip = [self buildSliderStrip];
|
||||||
|
|
||||||
|
[wrapper addSubview:header];
|
||||||
|
[wrapper addSubview:strip];
|
||||||
|
|
||||||
|
// Anchor header to top, strip to bottom. This is more robust.
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
[header.topAnchor constraintEqualToAnchor:wrapper.topAnchor],
|
||||||
|
[header.leadingAnchor constraintEqualToAnchor:wrapper.leadingAnchor],
|
||||||
|
[header.trailingAnchor constraintEqualToAnchor:wrapper.trailingAnchor],
|
||||||
|
|
||||||
|
[strip.bottomAnchor constraintEqualToAnchor:wrapper.bottomAnchor],
|
||||||
|
[strip.leadingAnchor constraintEqualToAnchor:wrapper.leadingAnchor],
|
||||||
|
[strip.trailingAnchor constraintEqualToAnchor:wrapper.trailingAnchor],
|
||||||
|
]];
|
||||||
|
|
||||||
|
// Install in glass (wrapper stretches to the glass edges via LiquidGlassView)
|
||||||
|
self.glass.contentView = wrapper;
|
||||||
|
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Public API
|
||||||
|
|
||||||
|
- (void)showHUDWithVolume:(double)volume usingMusicPlayer:(PlayerApplication*)player andLabel:(NSString*)label anchoredToStatusButton:(NSStatusBarButton *)button {
|
||||||
|
self.controlledPlayer = player;
|
||||||
|
|
||||||
|
if (volume > 1.0) volume = MAX(0.0, MIN(1.0, volume / 100.0));
|
||||||
|
self.slider.doubleValue = volume;
|
||||||
|
|
||||||
|
// Update header
|
||||||
|
self.appIconView.image = [player icon];
|
||||||
|
self.titleLabel.stringValue = label;
|
||||||
|
|
||||||
|
// Size fence each time
|
||||||
|
[_panel setContentSize:NSMakeSize(kHUDWidth, kHUDHeight)];
|
||||||
|
|
||||||
|
[self positionPanelBelowStatusButton:button];
|
||||||
|
|
||||||
|
// Animate the fade-in
|
||||||
|
|
||||||
|
// 1. If the panel is already visible, just update it.
|
||||||
|
// Otherwise, prepare for a fade-in animation.
|
||||||
|
if (!self.panel.isVisible) {
|
||||||
|
// Set the panel to be fully transparent before showing it
|
||||||
|
self.panel.alphaValue = 0.0;
|
||||||
|
[self.panel orderFront:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Animate the alpha value to 1.0 (fully opaque)
|
||||||
|
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
|
||||||
|
context.duration = kFadeInDuration;
|
||||||
|
context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
|
||||||
|
[[self.panel animator] setAlphaValue:1.0];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Reset the auto-hide timer
|
||||||
|
[self.hideTimer invalidate];
|
||||||
|
self.hideTimer = [NSTimer scheduledTimerWithTimeInterval:kAutoHide
|
||||||
|
target:self
|
||||||
|
selector:@selector(hide)
|
||||||
|
userInfo:nil
|
||||||
|
repeats:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (void)setVolume:(double)volume {
|
||||||
|
if (volume > 1.0) volume = MAX(0.0, MIN(1.0, volume / 100.0));
|
||||||
|
self.slider.doubleValue = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)hide {
|
||||||
|
// Invalidate the timer to prevent this method from being called again
|
||||||
|
[self.hideTimer invalidate];
|
||||||
|
self.hideTimer = nil;
|
||||||
|
|
||||||
|
// Animate the fade-out
|
||||||
|
|
||||||
|
// 1. Animate the alpha value down to 0.0 (fully transparent)
|
||||||
|
[NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) {
|
||||||
|
context.duration = kFadeOutDuration;
|
||||||
|
context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
|
||||||
|
[[self.panel animator] setAlphaValue:0.0];
|
||||||
|
} completionHandler:^{
|
||||||
|
// 2. AFTER the animation is complete, properly order the window out.
|
||||||
|
// This is crucial for performance and correctness.
|
||||||
|
[self.panel orderOut:nil];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
#pragma mark - Layout / Anchoring
|
||||||
|
|
||||||
|
- (void)positionPanelBelowStatusButton:(NSStatusBarButton *)button {
|
||||||
|
if (!button || !button.window) {
|
||||||
|
NSScreen *screen = NSScreen.mainScreen ?: NSScreen.screens.firstObject;
|
||||||
|
if (screen) {
|
||||||
|
NSRect vis = screen.visibleFrame;
|
||||||
|
CGFloat x = NSMidX(vis) - self.panel.frame.size.width/2.0;
|
||||||
|
CGFloat y = NSMidY(vis) - self.panel.frame.size.height/2.0;
|
||||||
|
[self.panel setFrameOrigin:NSMakePoint(round(x), round(y))];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSRect buttonRectInWindow = [button convertRect:button.bounds toView:nil];
|
||||||
|
NSRect buttonInScreen = [button.window convertRectToScreen:buttonRectInWindow];
|
||||||
|
|
||||||
|
NSSize size = NSMakeSize(kHUDWidth, kHUDHeight);
|
||||||
|
CGFloat x = NSMidX(buttonInScreen) - size.width / 2.0;
|
||||||
|
CGFloat y = NSMinY(buttonInScreen) - size.height - kBelowGap;
|
||||||
|
|
||||||
|
NSScreen *target = button.window.screen ?: NSScreen.mainScreen;
|
||||||
|
NSRect vis = target.visibleFrame;
|
||||||
|
|
||||||
|
if (y < NSMinY(vis)) y = NSMinY(vis) + 2.0;
|
||||||
|
|
||||||
|
CGFloat margin = 8.0;
|
||||||
|
x = MAX(NSMinX(vis) + margin, MIN(x, NSMaxX(vis) - margin - size.width));
|
||||||
|
|
||||||
|
[self.panel setFrame:NSMakeRect(round(x), round(y), size.width, size.height) display:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Glass
|
||||||
|
|
||||||
|
- (void)installGlassInto:(NSView *)host cornerRadius:(CGFloat)radius {
|
||||||
|
if (@available(macOS 26.0, *)) {
|
||||||
|
LiquidGlassView *glass = [LiquidGlassView glassWithStyle:NSGlassEffectViewStyleClear // Clear
|
||||||
|
cornerRadius:radius
|
||||||
|
tintColor:[NSColor colorWithCalibratedWhite:0 alpha:1]];
|
||||||
|
self.glass = glass;
|
||||||
|
|
||||||
|
// Enable the new vibrant rim here.
|
||||||
|
glass.hasVibrantRim = NO;
|
||||||
|
|
||||||
|
[host addSubview:glass];
|
||||||
|
glass.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
[glass.leadingAnchor constraintEqualToAnchor:host.leadingAnchor],
|
||||||
|
[glass.trailingAnchor constraintEqualToAnchor:host.trailingAnchor],
|
||||||
|
[glass.topAnchor constraintEqualToAnchor:host.topAnchor],
|
||||||
|
[glass.bottomAnchor constraintEqualToAnchor:host.bottomAnchor],
|
||||||
|
]];
|
||||||
|
|
||||||
|
// Optional Tahoe tuning:
|
||||||
|
[glass setVariantIfAvailable:5];
|
||||||
|
[glass setScrimStateIfAvailable:0];
|
||||||
|
[glass setSubduedStateIfAvailable:0];
|
||||||
|
|
||||||
|
// Setting adaptive appearance to 0 is the key to keeping it dark.
|
||||||
|
[glass setAdaptiveAppearanceIfAvailable:0];
|
||||||
|
[glass setUseReducedShadowRadiusIfAvailable:YES];
|
||||||
|
[glass setContentLensingIfAvailable:0];
|
||||||
|
} else {
|
||||||
|
// Fallback on earlier versions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional SwiftUI-like post-filters:
|
||||||
|
//[glass applyVisualAdjustmentsWithSaturation:1.5 brightness:0.2 blur:0.25];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Content
|
||||||
|
|
||||||
|
|
||||||
|
// In TahoeVolumeHUD.m
|
||||||
|
|
||||||
|
- (NSView *)buildSliderStrip {
|
||||||
|
NSView *strip = [NSView new];
|
||||||
|
strip.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
|
||||||
|
// Left and Right speaker glyphs are correct
|
||||||
|
NSImageView *iconLeft = [NSImageView new];
|
||||||
|
iconLeft.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
iconLeft.symbolConfiguration = [NSImageSymbolConfiguration configurationWithPointSize:14 weight:NSFontWeightSemibold];
|
||||||
|
iconLeft.image = [NSImage imageWithSystemSymbolName:@"speaker.fill" accessibilityDescription:nil];
|
||||||
|
iconLeft.contentTintColor = NSColor.labelColor;
|
||||||
|
[iconLeft setContentHuggingPriority:251 forOrientation:NSLayoutConstraintOrientationHorizontal];
|
||||||
|
[iconLeft setContentCompressionResistancePriority:751 forOrientation:NSLayoutConstraintOrientationHorizontal];
|
||||||
|
|
||||||
|
NSImageView *iconRight = [NSImageView new];
|
||||||
|
iconRight.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
iconRight.symbolConfiguration = [NSImageSymbolConfiguration configurationWithPointSize:14 weight:NSFontWeightSemibold];
|
||||||
|
iconRight.image = [NSImage imageWithSystemSymbolName:@"speaker.wave.2.fill" accessibilityDescription:nil];
|
||||||
|
iconRight.contentTintColor = NSColor.labelColor;
|
||||||
|
[iconRight setContentHuggingPriority:251 forOrientation:NSLayoutConstraintOrientationHorizontal];
|
||||||
|
[iconRight setContentCompressionResistancePriority:751 forOrientation:NSLayoutConstraintOrientationHorizontal];
|
||||||
|
|
||||||
|
// Create the slider
|
||||||
|
VolumeSlider *slider = [VolumeSlider new]; // Changed from HoverSlider
|
||||||
|
slider.minValue = 0.0;
|
||||||
|
slider.maxValue = 1.0;
|
||||||
|
slider.doubleValue = 0.6;
|
||||||
|
slider.trackingDelegate = self;
|
||||||
|
slider.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
slider.controlSize = NSControlSizeSmall;
|
||||||
|
|
||||||
|
// Assign our custom cell that handles all the drawing
|
||||||
|
VolumeSliderCell *cell = [VolumeSliderCell new]; // Changed from CustomVolumeSlider
|
||||||
|
cell.minValue = 0.0;
|
||||||
|
cell.maxValue = 1.0;
|
||||||
|
cell.controlSize = NSControlSizeSmall;
|
||||||
|
slider.cell = cell;
|
||||||
|
|
||||||
|
[slider setContentHuggingPriority:1 forOrientation:NSLayoutConstraintOrientationHorizontal];
|
||||||
|
[slider setContentCompressionResistancePriority:1 forOrientation:NSLayoutConstraintOrientationHorizontal];
|
||||||
|
self.slider = slider;
|
||||||
|
|
||||||
|
// Add views to the strip - NO MORE trackBackgroundView
|
||||||
|
[strip addSubview:iconLeft];
|
||||||
|
[strip addSubview:slider];
|
||||||
|
[strip addSubview:iconRight];
|
||||||
|
|
||||||
|
// Set up constraints
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
[strip.heightAnchor constraintEqualToConstant:36],
|
||||||
|
|
||||||
|
[iconLeft.leadingAnchor constraintEqualToAnchor:strip.leadingAnchor constant:12],
|
||||||
|
[iconLeft.centerYAnchor constraintEqualToAnchor:strip.centerYAnchor],
|
||||||
|
|
||||||
|
[iconRight.trailingAnchor constraintEqualToAnchor:strip.trailingAnchor constant:-12],
|
||||||
|
[iconRight.centerYAnchor constraintEqualToAnchor:strip.centerYAnchor],
|
||||||
|
|
||||||
|
// Slider is now constrained directly between the icons
|
||||||
|
[slider.leadingAnchor constraintEqualToAnchor:iconLeft.trailingAnchor constant:8],
|
||||||
|
[slider.trailingAnchor constraintEqualToAnchor:iconRight.leadingAnchor constant:-8],
|
||||||
|
[slider.centerYAnchor constraintEqualToAnchor:strip.centerYAnchor],
|
||||||
|
]];
|
||||||
|
|
||||||
|
return strip;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (NSView *)buildHeaderRow {
|
||||||
|
NSView *row = [NSView new];
|
||||||
|
row.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
|
||||||
|
// Icon (uses the same appIconView instance so other code can update it)
|
||||||
|
self.appIconView = [NSImageView new];
|
||||||
|
self.appIconView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
self.appIconView.imageScaling = NSImageScaleProportionallyUpOrDown;
|
||||||
|
self.appIconView.wantsLayer = YES;
|
||||||
|
self.appIconView.layer.cornerRadius = 6.0;
|
||||||
|
self.appIconView.layer.masksToBounds = YES;
|
||||||
|
|
||||||
|
// Title label
|
||||||
|
self.titleLabel = [NSTextField labelWithString:@"Place holder"];
|
||||||
|
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||||
|
self.titleLabel.font = [NSFont systemFontOfSize:12 weight:NSFontWeightSemibold];
|
||||||
|
self.titleLabel.textColor = [NSColor labelColor];
|
||||||
|
|
||||||
|
[row addSubview:self.appIconView];
|
||||||
|
[row addSubview:self.titleLabel];
|
||||||
|
|
||||||
|
// New constraints for better padding and alignment.
|
||||||
|
CGFloat topPadding = 12.0; // Increased to provide more space at the top.
|
||||||
|
CGFloat bottomPadding = 4.0; // Defines space between header and slider strip.
|
||||||
|
|
||||||
|
[NSLayoutConstraint activateConstraints:@[
|
||||||
|
// Icon constraints define the layout and padding for the header row.
|
||||||
|
[self.appIconView.leadingAnchor constraintEqualToAnchor:row.leadingAnchor constant:kSideInset],
|
||||||
|
[self.appIconView.topAnchor constraintEqualToAnchor:row.topAnchor constant:topPadding],
|
||||||
|
[self.appIconView.widthAnchor constraintEqualToConstant:18],
|
||||||
|
[self.appIconView.heightAnchor constraintEqualToConstant:18],
|
||||||
|
|
||||||
|
// The header row's height is determined by the icon's position and its own padding.
|
||||||
|
[row.bottomAnchor constraintEqualToAnchor:self.appIconView.bottomAnchor constant:bottomPadding],
|
||||||
|
|
||||||
|
// Title label is positioned relative to the icon.
|
||||||
|
[self.titleLabel.leadingAnchor constraintEqualToAnchor:self.appIconView.trailingAnchor constant:8],
|
||||||
|
[self.titleLabel.centerYAnchor constraintEqualToAnchor:self.appIconView.centerYAnchor],
|
||||||
|
[self.titleLabel.trailingAnchor constraintLessThanOrEqualToAnchor:row.trailingAnchor constant:-kSideInset],
|
||||||
|
]];
|
||||||
|
|
||||||
|
// Good contrast on glass
|
||||||
|
// row.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark];
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - VolumeSliderDelegate
|
||||||
|
|
||||||
|
// 5. IMPLEMENT the new delegate methods.
|
||||||
|
- (void)volumeSlider:(VolumeSlider *)slider didChangeValue:(double)value {
|
||||||
|
// This is now the method that gets called during a drag.
|
||||||
|
|
||||||
|
// Selector to match the protocol definition.
|
||||||
|
if ([self.delegate respondsToSelector:@selector(hud:didChangeVolume:forPlayer:)]) {
|
||||||
|
[self.delegate hud:self didChangeVolume:value forPlayer:self.controlledPlayer];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the auto-hide timer on every value change.
|
||||||
|
[self.hideTimer invalidate];
|
||||||
|
self.hideTimer = [NSTimer scheduledTimerWithTimeInterval:1.2
|
||||||
|
target:self
|
||||||
|
selector:@selector(hide)
|
||||||
|
userInfo:nil
|
||||||
|
repeats:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)volumeSliderDidEndDragging:(VolumeSlider *)slider {
|
||||||
|
if ([self.delegate respondsToSelector:@selector(didChangeVolumeFinal:)]) {
|
||||||
|
[self.delegate didChangeVolumeFinal:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
// You might also want to reset the hide timer here with a standard delay.
|
||||||
|
[self.hideTimer invalidate];
|
||||||
|
self.hideTimer = [NSTimer scheduledTimerWithTimeInterval:kAutoHide
|
||||||
|
target:self
|
||||||
|
selector:@selector(hide)
|
||||||
|
userInfo:nil
|
||||||
|
repeats:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
// VolumeSlider.h
|
||||||
|
// Volume Control
|
||||||
|
//
|
||||||
|
// Created by Andrea Alberti on 26.10.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
@class VolumeSlider;
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
// 1. Define a delegate protocol to communicate value changes.
|
||||||
|
@protocol VolumeSliderDelegate <NSObject>
|
||||||
|
- (void)volumeSlider:(VolumeSlider *)slider didChangeValue:(double)value;
|
||||||
|
- (void)volumeSliderDidEndDragging:(VolumeSlider *)slider;
|
||||||
|
@end
|
||||||
|
|
||||||
|
/**
|
||||||
|
An NSSlider subclass that detects mouse hover events and manually handles dragging
|
||||||
|
to ensure it works within a non-activating panel.
|
||||||
|
*/
|
||||||
|
@interface VolumeSlider : NSSlider
|
||||||
|
|
||||||
|
// 2. Add a delegate property.
|
||||||
|
@property (nonatomic, weak) id<VolumeSliderDelegate> trackingDelegate;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
//
|
||||||
|
// HoverSlider.m
|
||||||
|
// Volume Control
|
||||||
|
//
|
||||||
|
// Created by Andrea Alberti on 26.10.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "VolumeSlider.h"
|
||||||
|
#import "VolumeSliderCell.h" // We need this to access the 'isHovered' property
|
||||||
|
|
||||||
|
@implementation VolumeSlider
|
||||||
|
|
||||||
|
// This method sets up the tracking area that allows us to receive mouse events.
|
||||||
|
- (void)updateTrackingAreas {
|
||||||
|
[super updateTrackingAreas];
|
||||||
|
|
||||||
|
// Remove any old tracking areas to prevent duplicates
|
||||||
|
for (NSTrackingArea *area in self.trackingAreas) {
|
||||||
|
[self removeTrackingArea:area];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracking always active
|
||||||
|
NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways;
|
||||||
|
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds
|
||||||
|
options:options
|
||||||
|
owner:self
|
||||||
|
userInfo:nil];
|
||||||
|
[self addTrackingArea:trackingArea];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the mouse cursor enters the slider's bounds.
|
||||||
|
- (void)mouseEntered:(NSEvent *)event {
|
||||||
|
[super mouseEntered:event];
|
||||||
|
if ([self.cell isKindOfClass:[VolumeSliderCell class]]) {
|
||||||
|
// Tell our custom cell that it's being hovered
|
||||||
|
((VolumeSliderCell *)self.cell).isHovered = YES;
|
||||||
|
// Trigger a redraw to show the knob
|
||||||
|
[self setNeedsDisplay:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the mouse cursor leaves the slider's bounds.
|
||||||
|
- (void)mouseExited:(NSEvent *)event {
|
||||||
|
[super mouseExited:event];
|
||||||
|
if ([self.cell isKindOfClass:[VolumeSliderCell class]]) {
|
||||||
|
// Tell our custom cell that the hover is over
|
||||||
|
((VolumeSliderCell *)self.cell).isHovered = NO;
|
||||||
|
// Trigger a redraw to hide the knob
|
||||||
|
[self setNeedsDisplay:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateValueWithEvent:(NSEvent *)event {
|
||||||
|
// Convert the mouse click location to a point within the slider's bounds.
|
||||||
|
NSPoint point = [self convertPoint:[event locationInWindow] fromView:nil];
|
||||||
|
|
||||||
|
// Calculate the percentage value based on the horizontal position.
|
||||||
|
CGFloat percentage = point.x / self.bounds.size.width;
|
||||||
|
|
||||||
|
// Clamp the value between 0.0 and 1.0.
|
||||||
|
percentage = MAX(0.0, MIN(1.0, percentage));
|
||||||
|
|
||||||
|
// Calculate the actual slider value based on its min/max range.
|
||||||
|
double newValue = self.minValue + (percentage * (self.maxValue - self.minValue));
|
||||||
|
|
||||||
|
// Update the slider's own value and notify the delegate.
|
||||||
|
self.doubleValue = newValue;
|
||||||
|
[self.trackingDelegate volumeSlider:self didChangeValue:self.doubleValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)mouseDown:(NSEvent *)event {
|
||||||
|
[self updateValueWithEvent:event];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)mouseDragged:(NSEvent *)event {
|
||||||
|
[self updateValueWithEvent:event];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)mouseUp:(NSEvent *)event {
|
||||||
|
[self.trackingDelegate volumeSliderDidEndDragging:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// CustomVolumeSlider.h
|
||||||
|
// Volume Control
|
||||||
|
//
|
||||||
|
// Created by Andrea Alberti on 25.10.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <AppKit/NSSliderCell.h>
|
||||||
|
|
||||||
|
@interface VolumeSliderCell : NSSliderCell
|
||||||
|
|
||||||
|
// This property will control whether the knob is visible.
|
||||||
|
@property (nonatomic, assign) BOOL isHovered;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
//
|
||||||
|
// CustomVolumeSlider.m
|
||||||
|
// Volume Control
|
||||||
|
//
|
||||||
|
// Created by Andrea Alberti on 25.10.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "VolumeSliderCell.h"
|
||||||
|
#import <AppKit/NSColor.h>
|
||||||
|
#import <AppKit/NSBezierPath.h>
|
||||||
|
|
||||||
|
@implementation VolumeSliderCell
|
||||||
|
- (void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped {
|
||||||
|
// This rect is the area for the bar. We'll make it 4pt tall and centered.
|
||||||
|
rect = NSInsetRect(rect, 0, (NSHeight(rect) - 4.0) / 2.0);
|
||||||
|
|
||||||
|
// 1. Draw the background "track"
|
||||||
|
[[NSColor colorWithWhite:1.0 alpha:0.25] setFill];
|
||||||
|
NSBezierPath *backgroundPath = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:2 yRadius:2];
|
||||||
|
[backgroundPath fill];
|
||||||
|
|
||||||
|
// 2. Calculate the width of the "filled" portion
|
||||||
|
CGFloat percentage = (self.doubleValue - self.minValue) / (self.maxValue - self.minValue);
|
||||||
|
NSRect fillRect = rect;
|
||||||
|
fillRect.size.width = round(NSWidth(rect) * percentage);
|
||||||
|
|
||||||
|
// 3. Draw the active "fill" portion
|
||||||
|
[[NSColor colorWithWhite:1.0 alpha:0.85] setFill];
|
||||||
|
NSBezierPath *fillPath = [NSBezierPath bezierPathWithRoundedRect:fillRect xRadius:2 yRadius:2];
|
||||||
|
[fillPath fill];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)drawKnob:(NSRect)knobRect {
|
||||||
|
if (self.isHovered) {
|
||||||
|
/*
|
||||||
|
CGFloat d = 12;
|
||||||
|
knobRect = NSMakeRect(NSMidX(knobRect)-d/2.0, NSMidY(knobRect)-d/2.0, d, d);
|
||||||
|
[[NSColor whiteColor] setFill];
|
||||||
|
NSBezierPath *circle = [NSBezierPath bezierPathWithRoundedRect:knobRect xRadius:d/2 yRadius:d/2];
|
||||||
|
[circle fill];
|
||||||
|
*/
|
||||||
|
[super drawKnob:knobRect]; // Draws Apple's standard system knob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1002 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 905 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 888 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 911 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 518 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 855 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 901 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |