mirror of
https://github.com/kyleneideck/BackgroundMusic
synced 2024-11-22 12:13:03 +00:00
Move the output device menu items to the main menu.
I don't know why I put them in the Preferences menu initially. This is more convenient. Closes #170. Also: - Update the output device menu items as needed instead of when the user opens the menu. This saves a bit of CPU time and means if the user has the menu open, changes are made when they're needed instead of the next time the user opens the menu. - Fix BGMAppUITests::testCycleOutputDevices for the latest Xcode/macOS.
This commit is contained in:
parent
94f13e747c
commit
5e12f9fc01
12 changed files with 245 additions and 108 deletions
|
@ -300,8 +300,8 @@
|
||||||
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
|
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
|
||||||
1CD410D21F9EDDAD0070A094 /* BGMAppVolumesController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMAppVolumesController.h; sourceTree = "<group>"; };
|
1CD410D21F9EDDAD0070A094 /* BGMAppVolumesController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMAppVolumesController.h; sourceTree = "<group>"; };
|
||||||
1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAppVolumesController.mm; sourceTree = "<group>"; };
|
1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAppVolumesController.mm; sourceTree = "<group>"; };
|
||||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMOutputDevicePrefs.h; path = Preferences/BGMOutputDevicePrefs.h; sourceTree = "<group>"; };
|
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMOutputDevicePrefs.h; sourceTree = "<group>"; };
|
||||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMOutputDevicePrefs.mm; path = Preferences/BGMOutputDevicePrefs.mm; sourceTree = "<group>"; };
|
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMOutputDevicePrefs.mm; sourceTree = "<group>"; };
|
||||||
1CEACF4E1F34A30000FEC143 /* Mock_CAHALAudioSystemObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Mock_CAHALAudioSystemObject.cpp; path = UnitTests/Mock_CAHALAudioSystemObject.cpp; sourceTree = "<group>"; };
|
1CEACF4E1F34A30000FEC143 /* Mock_CAHALAudioSystemObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Mock_CAHALAudioSystemObject.cpp; path = UnitTests/Mock_CAHALAudioSystemObject.cpp; sourceTree = "<group>"; };
|
||||||
1CED61681C3081C2002CAFCF /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
1CED61681C3081C2002CAFCF /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||||
1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAudioDeviceManager.h; sourceTree = "<group>"; };
|
1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAudioDeviceManager.h; sourceTree = "<group>"; };
|
||||||
|
@ -419,8 +419,6 @@
|
||||||
27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */,
|
27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */,
|
||||||
1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */,
|
1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */,
|
||||||
1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */,
|
1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */,
|
||||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */,
|
|
||||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */,
|
|
||||||
);
|
);
|
||||||
name = "Preferences Menu";
|
name = "Preferences Menu";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -573,6 +571,8 @@
|
||||||
1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */,
|
1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */,
|
||||||
1C4699451BD5BF2E00F78043 /* Music Players */,
|
1C4699451BD5BF2E00F78043 /* Music Players */,
|
||||||
1C0BD0A21BF1A827004F4CF5 /* Preferences Menu */,
|
1C0BD0A21BF1A827004F4CF5 /* Preferences Menu */,
|
||||||
|
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */,
|
||||||
|
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */,
|
||||||
1C46994D1BD7694C00F78043 /* BGMDeviceControlSync.h */,
|
1C46994D1BD7694C00F78043 /* BGMDeviceControlSync.h */,
|
||||||
1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */,
|
1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */,
|
||||||
1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */,
|
1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */,
|
||||||
|
|
|
@ -25,17 +25,18 @@
|
||||||
|
|
||||||
// Local Includes
|
// Local Includes
|
||||||
#import "BGM_Utils.h"
|
#import "BGM_Utils.h"
|
||||||
#import "BGMUserDefaults.h"
|
#import "BGMAppVolumesController.h"
|
||||||
#import "BGMMusicPlayers.h"
|
|
||||||
#import "BGMAutoPauseMusic.h"
|
#import "BGMAutoPauseMusic.h"
|
||||||
#import "BGMAutoPauseMenuItem.h"
|
#import "BGMAutoPauseMenuItem.h"
|
||||||
#import "BGMSystemSoundsVolume.h"
|
#import "BGMMusicPlayers.h"
|
||||||
#import "BGMAppVolumesController.h"
|
#import "BGMOutputDevicePrefs.h"
|
||||||
|
#import "BGMOutputVolumeMenuItem.h"
|
||||||
#import "BGMPreferencesMenu.h"
|
#import "BGMPreferencesMenu.h"
|
||||||
#import "BGMPreferredOutputDevices.h"
|
#import "BGMPreferredOutputDevices.h"
|
||||||
#import "BGMXPCListener.h"
|
#import "BGMSystemSoundsVolume.h"
|
||||||
#import "BGMOutputVolumeMenuItem.h"
|
|
||||||
#import "BGMTermination.h"
|
#import "BGMTermination.h"
|
||||||
|
#import "BGMUserDefaults.h"
|
||||||
|
#import "BGMXPCListener.h"
|
||||||
#import "SystemPreferences.h"
|
#import "SystemPreferences.h"
|
||||||
|
|
||||||
// System Includes
|
// System Includes
|
||||||
|
@ -61,6 +62,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||||
BGMMusicPlayers* musicPlayers;
|
BGMMusicPlayers* musicPlayers;
|
||||||
BGMSystemSoundsVolume* systemSoundsVolume;
|
BGMSystemSoundsVolume* systemSoundsVolume;
|
||||||
BGMAppVolumesController* appVolumes;
|
BGMAppVolumesController* appVolumes;
|
||||||
|
BGMOutputDevicePrefs* outputDevicePrefs;
|
||||||
BGMPreferencesMenu* prefsMenu;
|
BGMPreferencesMenu* prefsMenu;
|
||||||
BGMXPCListener* xpcListener;
|
BGMXPCListener* xpcListener;
|
||||||
BGMPreferredOutputDevices* preferredOutputDevices;
|
BGMPreferredOutputDevices* preferredOutputDevices;
|
||||||
|
@ -292,9 +294,15 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
||||||
|
|
||||||
[self initVolumesMenuSection];
|
[self initVolumesMenuSection];
|
||||||
|
|
||||||
|
// Output device selection.
|
||||||
|
outputDevicePrefs = [[BGMOutputDevicePrefs alloc] initWithBGMMenu:self.bgmMenu
|
||||||
|
audioDevices:audioDevices
|
||||||
|
preferredDevices:preferredOutputDevices];
|
||||||
|
[audioDevices setOutputDevicePrefs:outputDevicePrefs];
|
||||||
|
|
||||||
|
// Preferences submenu.
|
||||||
prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu
|
prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu
|
||||||
audioDevices:audioDevices
|
audioDevices:audioDevices
|
||||||
preferredDevices:preferredOutputDevices
|
|
||||||
musicPlayers:musicPlayers
|
musicPlayers:musicPlayers
|
||||||
aboutPanel:self.aboutPanel
|
aboutPanel:self.aboutPanel
|
||||||
aboutPanelLicenseView:self.aboutPanelLicenseView];
|
aboutPanelLicenseView:self.aboutPanelLicenseView];
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
|
|
||||||
// Forward Declarations
|
// Forward Declarations
|
||||||
@class BGMOutputVolumeMenuItem;
|
@class BGMOutputVolumeMenuItem;
|
||||||
|
@class BGMOutputDevicePrefs;
|
||||||
|
|
||||||
|
|
||||||
#pragma clang assume_nonnull begin
|
#pragma clang assume_nonnull begin
|
||||||
|
@ -54,6 +55,9 @@ static const int kBGMErrorCode_ReturningEarly = 2;
|
||||||
// Set the BGMOutputVolumeMenuItem to be notified when the output device is changed.
|
// Set the BGMOutputVolumeMenuItem to be notified when the output device is changed.
|
||||||
- (void) setOutputVolumeMenuItem:(BGMOutputVolumeMenuItem*)item;
|
- (void) setOutputVolumeMenuItem:(BGMOutputVolumeMenuItem*)item;
|
||||||
|
|
||||||
|
// Set the BGMOutputDevicePrefs to be notified when the output device is changed.
|
||||||
|
- (void) setOutputDevicePrefs:(BGMOutputDevicePrefs*)prefs;
|
||||||
|
|
||||||
// Set BGMDevice as the default audio device for all processes
|
// Set BGMDevice as the default audio device for all processes
|
||||||
- (NSError* __nullable) setBGMDeviceAsOSDefault;
|
- (NSError* __nullable) setBGMDeviceAsOSDefault;
|
||||||
// Replace BGMDevice as the default device with the output device
|
// Replace BGMDevice as the default device with the output device
|
||||||
|
|
|
@ -26,16 +26,17 @@
|
||||||
// Local Includes
|
// Local Includes
|
||||||
#import "BGM_Types.h"
|
#import "BGM_Types.h"
|
||||||
#import "BGM_Utils.h"
|
#import "BGM_Utils.h"
|
||||||
#import "BGMDeviceControlSync.h"
|
|
||||||
#import "BGMPlayThrough.h"
|
|
||||||
#import "BGMAudioDevice.h"
|
#import "BGMAudioDevice.h"
|
||||||
#import "BGMXPCProtocols.h"
|
#import "BGMDeviceControlSync.h"
|
||||||
|
#import "BGMOutputDevicePrefs.h"
|
||||||
#import "BGMOutputVolumeMenuItem.h"
|
#import "BGMOutputVolumeMenuItem.h"
|
||||||
|
#import "BGMPlayThrough.h"
|
||||||
|
#import "BGMXPCProtocols.h"
|
||||||
|
|
||||||
// PublicUtility Includes
|
// PublicUtility Includes
|
||||||
#import "CAHALAudioSystemObject.h"
|
|
||||||
#import "CAAutoDisposer.h"
|
|
||||||
#import "CAAtomic.h"
|
#import "CAAtomic.h"
|
||||||
|
#import "CAAutoDisposer.h"
|
||||||
|
#import "CAHALAudioSystemObject.h"
|
||||||
|
|
||||||
|
|
||||||
#pragma clang assume_nonnull begin
|
#pragma clang assume_nonnull begin
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
NSXPCConnection* __nullable bgmXPCHelperConnection;
|
NSXPCConnection* __nullable bgmXPCHelperConnection;
|
||||||
|
|
||||||
BGMOutputVolumeMenuItem* __nullable outputVolumeMenuItem;
|
BGMOutputVolumeMenuItem* __nullable outputVolumeMenuItem;
|
||||||
|
BGMOutputDevicePrefs* __nullable outputDevicePrefs;
|
||||||
|
|
||||||
NSRecursiveLock* stateLock;
|
NSRecursiveLock* stateLock;
|
||||||
}
|
}
|
||||||
|
@ -71,6 +73,7 @@
|
||||||
stateLock = [NSRecursiveLock new];
|
stateLock = [NSRecursiveLock new];
|
||||||
bgmXPCHelperConnection = nil;
|
bgmXPCHelperConnection = nil;
|
||||||
outputVolumeMenuItem = nil;
|
outputVolumeMenuItem = nil;
|
||||||
|
outputDevicePrefs = nil;
|
||||||
outputDevice = kAudioObjectUnknown;
|
outputDevice = kAudioObjectUnknown;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -102,6 +105,10 @@
|
||||||
outputVolumeMenuItem = item;
|
outputVolumeMenuItem = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void) setOutputDevicePrefs:(BGMOutputDevicePrefs*)prefs {
|
||||||
|
outputDevicePrefs = prefs;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark Systemwide Default Device
|
#pragma mark Systemwide Default Device
|
||||||
|
|
||||||
// Note that there are two different "default" output devices on OS X: "output" and "system output". See
|
// Note that there are two different "default" output devices on OS X: "output" and "system output". See
|
||||||
|
@ -324,6 +331,7 @@
|
||||||
|
|
||||||
// Update the menu item for the volume of the output device.
|
// Update the menu item for the volume of the output device.
|
||||||
[outputVolumeMenuItem outputDeviceDidChange];
|
[outputVolumeMenuItem outputDeviceDidChange];
|
||||||
|
[outputDevicePrefs outputDeviceDidChange];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSError*) failedToSetOutputDevice:(AudioDeviceID)deviceID
|
- (NSError*) failedToSetOutputDevice:(AudioDeviceID)deviceID
|
||||||
|
|
|
@ -32,9 +32,13 @@
|
||||||
|
|
||||||
@interface BGMOutputDevicePrefs : NSObject
|
@interface BGMOutputDevicePrefs : NSObject
|
||||||
|
|
||||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
- (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices;
|
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||||
- (void) populatePreferencesMenu:(NSMenu*)prefsMenu;
|
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices;
|
||||||
|
|
||||||
|
// To be called when BGMApp has been set to use a different output device. For example, when a new
|
||||||
|
// device is connected and BGMPreferredOutputDevices decides BGMApp should switch to it.
|
||||||
|
- (void) outputDeviceDidChange;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -29,36 +29,111 @@
|
||||||
#import "BGMAudioDevice.h"
|
#import "BGMAudioDevice.h"
|
||||||
|
|
||||||
// PublicUtility Includes
|
// PublicUtility Includes
|
||||||
#include "CAHALAudioSystemObject.h"
|
#import "CAAutoDisposer.h"
|
||||||
#include "CAHALAudioDevice.h"
|
#import "CAHALAudioDevice.h"
|
||||||
#include "CAAutoDisposer.h"
|
#import "CAHALAudioSystemObject.h"
|
||||||
|
#import "CAPropertyAddress.h"
|
||||||
|
|
||||||
|
// STL Includes
|
||||||
|
#import <set>
|
||||||
|
|
||||||
|
|
||||||
#pragma clang assume_nonnull begin
|
#pragma clang assume_nonnull begin
|
||||||
|
|
||||||
static NSInteger const kOutputDeviceMenuItemTag = 2;
|
static NSInteger const kOutputDeviceMenuItemTag = 5;
|
||||||
|
|
||||||
@implementation BGMOutputDevicePrefs {
|
@implementation BGMOutputDevicePrefs {
|
||||||
|
NSMenu* bgmMenu;
|
||||||
BGMAudioDeviceManager* audioDevices;
|
BGMAudioDeviceManager* audioDevices;
|
||||||
BGMPreferredOutputDevices* preferredDevices;
|
BGMPreferredOutputDevices* preferredDevices;
|
||||||
NSMutableArray<NSMenuItem*>* outputDeviceMenuItems;
|
NSMutableArray<NSMenuItem*>* outputDeviceMenuItems;
|
||||||
|
// Called when a CoreAudio property has changed and we might need to update the menu. For
|
||||||
|
// example, when a device is connected or disconnected.
|
||||||
|
AudioObjectPropertyListenerBlock refreshNeededListener;
|
||||||
|
// The devices we've added refreshNeededListener to. Used to avoid adding it to a device twice
|
||||||
|
// for the same property and to remove it from all devices in dealloc.
|
||||||
|
std::set<BGMAudioDevice> listenedDevices_kAudioDevicePropertyDataSources;
|
||||||
|
std::set<BGMAudioDevice> listenedDevices_kAudioDevicePropertyDataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
- (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices {
|
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||||
|
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices {
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
|
bgmMenu = inBGMMenu;
|
||||||
audioDevices = inAudioDevices;
|
audioDevices = inAudioDevices;
|
||||||
preferredDevices = inPreferredDevices;
|
preferredDevices = inPreferredDevices;
|
||||||
outputDeviceMenuItems = [NSMutableArray new];
|
outputDeviceMenuItems = [NSMutableArray new];
|
||||||
|
|
||||||
|
[self listenForDevicesAddedOrRemoved];
|
||||||
|
[self populateBGMMenu];
|
||||||
}
|
}
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) populatePreferencesMenu:(NSMenu*)prefsMenu {
|
- (void) dealloc {
|
||||||
|
// Tell CoreAudio not to call the listener block anymore. This probably isn't necessary.
|
||||||
|
//
|
||||||
|
// I think it's safe to do this without dispatching to the main queue because dealloc and
|
||||||
|
// refreshNeededListener should be essentially mutually exclusive. If refreshNeededListener is
|
||||||
|
// invoked and gets a value for weakSelf, it holds the strong ref until it returns, so dealloc
|
||||||
|
// won't be called. If refreshNeededListener is invoked and deallocation has started, it will
|
||||||
|
// get nil for weakSelf and just return.
|
||||||
|
auto removeListener = [&] (CAHALAudioObject audioObject, AudioObjectPropertySelector prop) {
|
||||||
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
|
// Check the object still exists first to reduce unnecessary error logs.
|
||||||
|
if (CAHALAudioObject::ObjectExists(audioObject.GetObjectID())) {
|
||||||
|
audioObject.RemovePropertyListenerBlock(CAPropertyAddress(prop),
|
||||||
|
dispatch_get_main_queue(),
|
||||||
|
refreshNeededListener);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the listener from each audio object we added it to.
|
||||||
|
removeListener(CAHALAudioSystemObject(), kAudioHardwarePropertyDevices);
|
||||||
|
|
||||||
|
for (auto device : listenedDevices_kAudioDevicePropertyDataSources) {
|
||||||
|
removeListener(device, kAudioDevicePropertyDataSources);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto device : listenedDevices_kAudioDevicePropertyDataSource) {
|
||||||
|
removeListener(device, kAudioDevicePropertyDataSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) listenForDevicesAddedOrRemoved {
|
||||||
|
// Create the block that will run when a device is added or removed.
|
||||||
|
BGMOutputDevicePrefs* __weak weakSelf = self;
|
||||||
|
|
||||||
|
refreshNeededListener = ^(UInt32 inNumberAddresses,
|
||||||
|
const AudioObjectPropertyAddress* inAddresses) {
|
||||||
|
#pragma unused (inNumberAddresses, inAddresses)
|
||||||
|
|
||||||
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
|
[weakSelf populateBGMMenu];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the listener block to be called when devices are connected or disconnected.
|
||||||
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
|
CAHALAudioSystemObject().AddPropertyListenerBlock(
|
||||||
|
CAPropertyAddress(kAudioHardwarePropertyDevices),
|
||||||
|
dispatch_get_main_queue(),
|
||||||
|
refreshNeededListener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) populateBGMMenu {
|
||||||
|
// TODO: Technically, we should assert we're on the main queue rather than just the main thread.
|
||||||
|
BGMAssert([NSThread isMainThread],
|
||||||
|
"BGMOutputDevicePrefs::populateBGMMenu called on non-main thread");
|
||||||
|
|
||||||
// Remove existing menu items
|
// Remove existing menu items
|
||||||
for (NSMenuItem* item in outputDeviceMenuItems) {
|
for (NSMenuItem* item in outputDeviceMenuItems) {
|
||||||
[prefsMenu removeItem:item];
|
DebugMsg("BGMOutputDevicePrefs::populateBGMMenu: Removing %s", item.description.UTF8String);
|
||||||
|
[bgmMenu removeItem:item];
|
||||||
}
|
}
|
||||||
|
|
||||||
[outputDeviceMenuItems removeAllObjects];
|
[outputDeviceMenuItems removeAllObjects];
|
||||||
|
@ -72,25 +147,51 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||||
audioSystem.GetAudioDevices(numDevices, devices);
|
audioSystem.GetAudioDevices(numDevices, devices);
|
||||||
|
|
||||||
for (UInt32 i = 0; i < numDevices; i++) {
|
for (UInt32 i = 0; i < numDevices; i++) {
|
||||||
[self insertMenuItemsForDevice:devices[i] preferencesMenu:prefsMenu];
|
[self insertMenuItemsForDevice:devices[i]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) insertMenuItemsForDevice:(BGMAudioDevice)device preferencesMenu:(NSMenu*)prefsMenu {
|
- (void) insertMenuItemsForDevice:(BGMAudioDevice)device {
|
||||||
// Insert menu items after the item for the "Output Device" heading.
|
// Insert menu items after the item for the "Output Device" heading.
|
||||||
const NSInteger menuItemsIdx = [prefsMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
|
const NSInteger menuItemsIdx = [bgmMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
|
||||||
|
|
||||||
BOOL canBeOutputDevice = YES;
|
BOOL canBeOutputDevice = YES;
|
||||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::insertMenuItemsForDevice", ([&] {
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
canBeOutputDevice = device.CanBeOutputDeviceInBGMApp();
|
canBeOutputDevice = device.CanBeOutputDeviceInBGMApp();
|
||||||
}));
|
});
|
||||||
|
|
||||||
if (canBeOutputDevice) {
|
if (canBeOutputDevice) {
|
||||||
for (NSMenuItem* item : [self createMenuItemsForDevice:device]) {
|
for (NSMenuItem* item : [self createMenuItemsForDevice:device]) {
|
||||||
[prefsMenu insertItem:item atIndex:menuItemsIdx];
|
DebugMsg("BGMOutputDevicePrefs::insertMenuItemsForDevice: Inserting %s",
|
||||||
|
item.description.UTF8String);
|
||||||
|
[bgmMenu insertItem:item atIndex:menuItemsIdx];
|
||||||
[outputDeviceMenuItems addObject:item];
|
[outputDeviceMenuItems addObject:item];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add listeners to update the menu when the device's data source changes or it changes its
|
||||||
|
// list of data sources. We do this so that, for example, when you plug headphones into the
|
||||||
|
// built-in jack, the menu item for the built-in device will change from "Internal Speakers"
|
||||||
|
// to "Headphones".
|
||||||
|
if (listenedDevices_kAudioDevicePropertyDataSources.count(device) == 0) {
|
||||||
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
|
device.AddPropertyListenerBlock(CAPropertyAddress(kAudioDevicePropertyDataSources,
|
||||||
|
kAudioDevicePropertyScopeOutput),
|
||||||
|
dispatch_get_main_queue(),
|
||||||
|
refreshNeededListener);
|
||||||
|
listenedDevices_kAudioDevicePropertyDataSources.insert(device);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (listenedDevices_kAudioDevicePropertyDataSource.count(device) == 0) {
|
||||||
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
|
device.AddPropertyListenerBlock(CAPropertyAddress(kAudioDevicePropertyDataSource,
|
||||||
|
kAudioDevicePropertyScopeOutput),
|
||||||
|
dispatch_get_main_queue(),
|
||||||
|
refreshNeededListener);
|
||||||
|
listenedDevices_kAudioDevicePropertyDataSource.insert(device);
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,8 +210,8 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||||
// TODO: Handle data destinations as well? I don't have (or know of) any hardware with them.
|
// TODO: Handle data destinations as well? I don't have (or know of) any hardware with them.
|
||||||
// TODO: Use the current data source's name when the control isn't settable, but only add one menu item.
|
// TODO: Use the current data source's name when the control isn't settable, but only add one menu item.
|
||||||
UInt32 numDataSources = 0;
|
UInt32 numDataSources = 0;
|
||||||
|
|
||||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", [&] {
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
if (device.HasDataSourceControl(scope, channel) &&
|
if (device.HasDataSourceControl(scope, channel) &&
|
||||||
device.DataSourceControlIsSettable(scope, channel)) {
|
device.DataSourceControlIsSettable(scope, channel)) {
|
||||||
numDataSources = device.GetNumberAvailableDataSources(scope, channel);
|
numDataSources = device.GetNumberAvailableDataSources(scope, channel);
|
||||||
|
@ -126,15 +227,14 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||||
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u %s%u",
|
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u %s%u",
|
||||||
"Device ID:", device.GetObjectID(),
|
"Device ID:", device.GetObjectID(),
|
||||||
", Data source ID:", dataSourceIDs[i]);
|
", Data source ID:", dataSourceIDs[i]);
|
||||||
|
|
||||||
BGMLogAndSwallowExceptionsMsg("BGMOutputDevicePrefs::createMenuItemsForDevice", "(DS)", [&]() {
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, "(DS)", [&] {
|
||||||
NSNumber* dataSourceID = [NSNumber numberWithUnsignedInt:dataSourceIDs[i]];
|
|
||||||
NSString* dataSourceName =
|
NSString* dataSourceName =
|
||||||
CFBridgingRelease(device.CopyDataSourceNameForID(scope, channel, dataSourceIDs[i]));
|
CFBridgingRelease(device.CopyDataSourceNameForID(scope, channel, dataSourceIDs[i]));
|
||||||
NSString* deviceName = CFBridgingRelease(device.CopyName());
|
NSString* deviceName = CFBridgingRelease(device.CopyName());
|
||||||
|
|
||||||
[items addObject:[self createMenuItemForDevice:device
|
[items addObject:[self createMenuItemForDevice:device
|
||||||
dataSourceID:dataSourceID
|
dataSourceID:@(dataSourceIDs[i])
|
||||||
title:dataSourceName
|
title:dataSourceName
|
||||||
toolTip:deviceName]];
|
toolTip:deviceName]];
|
||||||
});
|
});
|
||||||
|
@ -142,13 +242,13 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||||
} else {
|
} else {
|
||||||
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u",
|
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u",
|
||||||
"Device ID:", device.GetObjectID());
|
"Device ID:", device.GetObjectID());
|
||||||
|
|
||||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", ([&] {
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
[items addObject:[self createMenuItemForDevice:device
|
[items addObject:[self createMenuItemForDevice:device
|
||||||
dataSourceID:nil
|
dataSourceID:nil
|
||||||
title:CFBridgingRelease(device.CopyName())
|
title:CFBridgingRelease(device.CopyName())
|
||||||
toolTip:nil]];
|
toolTip:nil]];
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
@ -164,13 +264,13 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:BGMNN(title)
|
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:BGMNN(title)
|
||||||
action:@selector(outputDeviceWasChanged:)
|
action:@selector(outputDeviceMenuItemSelected:)
|
||||||
keyEquivalent:@""];
|
keyEquivalent:@""];
|
||||||
|
|
||||||
// Add the AirPlay icon to the labels of AirPlay devices.
|
// Add the AirPlay icon to the labels of AirPlay devices.
|
||||||
//
|
//
|
||||||
// TODO: Test this with real hardware that supports AirPlay. (I don't have any.)
|
// TODO: Test this with real hardware that supports AirPlay. (I don't have any.)
|
||||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemForDevice", [&] {
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
if (device.GetTransportType() == kAudioDeviceTransportTypeAirPlay) {
|
if (device.GetTransportType() == kAudioDeviceTransportTypeAirPlay) {
|
||||||
item.image = [NSImage imageNamed:@"AirPlayIcon"];
|
item.image = [NSImage imageNamed:@"AirPlayIcon"];
|
||||||
|
|
||||||
|
@ -192,12 +292,28 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||||
item.indentationLevel = 1;
|
item.indentationLevel = 1;
|
||||||
item.representedObject = @{ @"deviceID": @(device.GetObjectID()),
|
item.representedObject = @{ @"deviceID": @(device.GetObjectID()),
|
||||||
@"dataSourceID": dataSourceID ? BGMNN(dataSourceID) : [NSNull null] };
|
@"dataSourceID": dataSourceID ? BGMNN(dataSourceID) : [NSNull null] };
|
||||||
|
|
||||||
|
if (@available(macOS 10.10, *)) {
|
||||||
|
// Used for UI tests.
|
||||||
|
item.accessibilityIdentifier = @"output-device";
|
||||||
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) outputDeviceWasChanged:(NSMenuItem*)menuItem {
|
// Called by BGMAudioDeviceManager to tell us a different device has been set as the output device.
|
||||||
DebugMsg("BGMOutputDevicePrefs::outputDeviceWasChanged: '%s' menu item selected",
|
- (void) outputDeviceDidChange {
|
||||||
|
BGMOutputDevicePrefs* __weak weakSelf = self;
|
||||||
|
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||||
|
[weakSelf populateBGMMenu];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) outputDeviceMenuItemSelected:(NSMenuItem*)menuItem {
|
||||||
|
DebugMsg("BGMOutputDevicePrefs::outputDeviceMenuItemSelected: '%s' menu item selected",
|
||||||
[menuItem.title UTF8String]);
|
[menuItem.title UTF8String]);
|
||||||
|
|
||||||
// Make sure the menu item is actually for an output device.
|
// Make sure the menu item is actually for an output device.
|
||||||
|
@ -220,14 +336,14 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||||
[NSString stringWithFormat:@"%@ (%@)", menuItem.title, menuItem.toolTip] :
|
[NSString stringWithFormat:@"%@ (%@)", menuItem.title, menuItem.toolTip] :
|
||||||
menuItem.title;
|
menuItem.title;
|
||||||
|
|
||||||
|
if (changingDevice) {
|
||||||
|
// Add the new output device to the list of preferred devices.
|
||||||
|
[preferredDevices userChangedOutputDeviceTo:newDeviceID];
|
||||||
|
}
|
||||||
|
|
||||||
// Dispatched because it usually blocks. (Note that we're using
|
// Dispatched because it usually blocks. (Note that we're using
|
||||||
// DISPATCH_QUEUE_PRIORITY_HIGH, which is the second highest priority.)
|
// DISPATCH_QUEUE_PRIORITY_HIGH, which is the second highest priority.)
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||||
if (changingDevice) {
|
|
||||||
// Add the new output device to the list of preferred devices.
|
|
||||||
[preferredDevices userChangedOutputDeviceTo:newDeviceID];
|
|
||||||
}
|
|
||||||
|
|
||||||
[self changeToOutputDevice:newDeviceID
|
[self changeToOutputDevice:newDeviceID
|
||||||
newDataSource:newDataSourceID
|
newDataSource:newDataSourceID
|
||||||
deviceName:deviceName];
|
deviceName:deviceName];
|
|
@ -210,6 +210,14 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
||||||
- (void) removeOutputDeviceDataSourceListener {
|
- (void) removeOutputDeviceDataSourceListener {
|
||||||
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::removeOutputDeviceDataSourceListener",
|
BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::removeOutputDeviceDataSourceListener",
|
||||||
([&] {
|
([&] {
|
||||||
|
// Technically, there's a race here in that the device could be removed after we check it
|
||||||
|
// exists, but before we try to remove the listener. We could check the error code of the
|
||||||
|
// exception and not log an error message if the code is kAudioHardwareBadObjectError or
|
||||||
|
// kAudioHardwareBadDeviceError, but it probably wouldn't be worth the effort.
|
||||||
|
//
|
||||||
|
// So for now the main reason for checking the device exists here is that it makes debug
|
||||||
|
// builds much less likely to crash here. (They crash/break when an error is logged so it
|
||||||
|
// will be noticed.)
|
||||||
if (CAHALAudioObject::ObjectExists(outputDevice)) {
|
if (CAHALAudioObject::ObjectExists(outputDevice)) {
|
||||||
outputDevice.RemovePropertyListenerBlock(
|
outputDevice.RemovePropertyListenerBlock(
|
||||||
CAPropertyAddress(kAudioDevicePropertyDataSource, kScope),
|
CAPropertyAddress(kAudioDevicePropertyDataSource, kScope),
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?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="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<development version="8000" identifier="xcode"/>
|
<development version="8000" identifier="xcode"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
|
||||||
<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>
|
||||||
|
@ -40,6 +40,10 @@
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem isSeparatorItem="YES" tag="4" id="rkf-nx-H2J"/>
|
<menuItem isSeparatorItem="YES" tag="4" id="rkf-nx-H2J"/>
|
||||||
|
<menuItem title="Output Device" tag="5" enabled="NO" id="chk-9C-pab">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="D3z-zv-JrJ"/>
|
||||||
<menuItem title="Preferences" tag="1" id="BHb-uh-9Zm">
|
<menuItem title="Preferences" tag="1" id="BHb-uh-9Zm">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
<menu key="submenu" title="Preferences" id="Img-Dh-cpU">
|
<menu key="submenu" title="Preferences" id="Img-Dh-cpU">
|
||||||
|
@ -48,10 +52,6 @@
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem isSeparatorItem="YES" id="nb1-jq-97L"/>
|
<menuItem isSeparatorItem="YES" id="nb1-jq-97L"/>
|
||||||
<menuItem title="Output Device" tag="2" enabled="NO" id="chk-9C-pab">
|
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
|
||||||
</menuItem>
|
|
||||||
<menuItem isSeparatorItem="YES" id="D3z-zv-JrJ"/>
|
|
||||||
<menuItem title="About Background Music" tag="3" id="R45-Vo-Eto">
|
<menuItem title="About Background Music" tag="3" id="R45-Vo-Eto">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
|
|
||||||
// Local Includes
|
// Local Includes
|
||||||
#import "BGMAudioDeviceManager.h"
|
#import "BGMAudioDeviceManager.h"
|
||||||
#import "BGMPreferredOutputDevices.h"
|
|
||||||
#import "BGMMusicPlayers.h"
|
#import "BGMMusicPlayers.h"
|
||||||
|
|
||||||
// System Includes
|
// System Includes
|
||||||
|
@ -34,11 +33,10 @@
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface BGMPreferencesMenu : NSObject <NSMenuDelegate>
|
@interface BGMPreferencesMenu : NSObject
|
||||||
|
|
||||||
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices
|
|
||||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
||||||
aboutPanel:(NSPanel*)inAboutPanel
|
aboutPanel:(NSPanel*)inAboutPanel
|
||||||
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;
|
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
|
|
||||||
// Local Includes
|
// Local Includes
|
||||||
#import "BGMAutoPauseMusicPrefs.h"
|
#import "BGMAutoPauseMusicPrefs.h"
|
||||||
#import "BGMOutputDevicePrefs.h"
|
|
||||||
#import "BGMAboutPanel.h"
|
#import "BGMAboutPanel.h"
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +37,6 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
|
||||||
@implementation BGMPreferencesMenu {
|
@implementation BGMPreferencesMenu {
|
||||||
// Menu sections
|
// Menu sections
|
||||||
BGMAutoPauseMusicPrefs* autoPauseMusicPrefs;
|
BGMAutoPauseMusicPrefs* autoPauseMusicPrefs;
|
||||||
BGMOutputDevicePrefs* outputDevicePrefs;
|
|
||||||
|
|
||||||
// The About Background Music window
|
// The About Background Music window
|
||||||
BGMAboutPanel* aboutPanel;
|
BGMAboutPanel* aboutPanel;
|
||||||
|
@ -46,21 +44,16 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
|
||||||
|
|
||||||
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices
|
|
||||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
||||||
aboutPanel:(NSPanel*)inAboutPanel
|
aboutPanel:(NSPanel*)inAboutPanel
|
||||||
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView {
|
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView {
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
NSMenu* prefsMenu = [[inBGMMenu itemWithTag:kPreferencesMenuItemTag] submenu];
|
NSMenu* prefsMenu = [[inBGMMenu itemWithTag:kPreferencesMenuItemTag] submenu];
|
||||||
[prefsMenu setDelegate:self];
|
|
||||||
|
|
||||||
autoPauseMusicPrefs = [[BGMAutoPauseMusicPrefs alloc] initWithPreferencesMenu:prefsMenu
|
autoPauseMusicPrefs = [[BGMAutoPauseMusicPrefs alloc] initWithPreferencesMenu:prefsMenu
|
||||||
audioDevices:inAudioDevices
|
audioDevices:inAudioDevices
|
||||||
musicPlayers:inMusicPlayers];
|
musicPlayers:inMusicPlayers];
|
||||||
|
|
||||||
outputDevicePrefs = [[BGMOutputDevicePrefs alloc] initWithAudioDevices:inAudioDevices
|
|
||||||
preferredDevices:inPreferredDevices];
|
|
||||||
|
|
||||||
aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView];
|
aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView];
|
||||||
|
|
||||||
// Set up the "About Background Music" menu item
|
// Set up the "About Background Music" menu item
|
||||||
|
@ -72,12 +65,6 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark NSMenuDelegate
|
|
||||||
|
|
||||||
- (void) menuNeedsUpdate:(NSMenu*)menu {
|
|
||||||
[outputDevicePrefs populatePreferencesMenu:menu];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* BGMApp.h
|
* BGMApp.h
|
||||||
|
*
|
||||||
|
* Generated with
|
||||||
|
* sdef "/Applications/Background Music.app" | sdp -fh --basename BGMApp
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#import <AppKit/AppKit.h>
|
#import <AppKit/AppKit.h>
|
||||||
|
@ -14,11 +17,11 @@
|
||||||
* Background Music
|
* Background Music
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// an output audio device
|
// A hardware device that can play audio
|
||||||
@interface BGMAppOutputDevice : SBObject
|
@interface BGMAppOutputDevice : SBObject
|
||||||
|
|
||||||
@property (copy, readonly) NSString *name;
|
@property (copy, readonly) NSString *name; // The name of the output device.
|
||||||
@property BOOL selected; // is this the device to be used for audio output?
|
@property BOOL selected; // Is this the device to be used for audio output?
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@ -27,5 +30,7 @@
|
||||||
|
|
||||||
- (SBElementArray<BGMAppOutputDevice *> *) outputDevices;
|
- (SBElementArray<BGMAppOutputDevice *> *) outputDevices;
|
||||||
|
|
||||||
|
@property (copy) BGMAppOutputDevice *selectedOutputDevice; // The device to be used for audio output
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
// BGMAppUITests.mm
|
// BGMAppUITests.mm
|
||||||
// BGMAppUITests
|
// BGMAppUITests
|
||||||
//
|
//
|
||||||
// Copyright © 2017 Kyle Neideck
|
// Copyright © 2017, 2018 Kyle Neideck
|
||||||
//
|
//
|
||||||
// You might want to use Xcode's UI test recording feature if you add new tests.
|
// You might want to use Xcode's UI test recording feature if you add new tests.
|
||||||
//
|
//
|
||||||
|
@ -60,8 +60,8 @@
|
||||||
// Set up the app object and some convenience vars.
|
// Set up the app object and some convenience vars.
|
||||||
app = [[XCUIApplication alloc] init];
|
app = [[XCUIApplication alloc] init];
|
||||||
menuItems = app.menuBars.menuItems;
|
menuItems = app.menuBars.menuItems;
|
||||||
icon = [app.menuBars childrenMatchingType:XCUIElementTypeMenuBarItem].element;
|
|
||||||
prefs = menuItems[@"Preferences"];
|
prefs = menuItems[@"Preferences"];
|
||||||
|
icon = [app.menuBars childrenMatchingType:XCUIElementTypeStatusItem].element;
|
||||||
|
|
||||||
// TODO: Make sure BGMDevice isn't set as the OS X default device before launching BGMApp.
|
// TODO: Make sure BGMDevice isn't set as the OS X default device before launching BGMApp.
|
||||||
|
|
||||||
|
@ -72,6 +72,20 @@
|
||||||
|
|
||||||
// Launch BGMApp.
|
// Launch BGMApp.
|
||||||
[app launch];
|
[app launch];
|
||||||
|
|
||||||
|
if (![icon waitForExistenceWithTimeout:1.0]) {
|
||||||
|
// The status bar icon/button has this type when using older versions of XCTest, so try
|
||||||
|
// both. (Actually, it might depend on the macOS or Xcode version. I'm not sure.)
|
||||||
|
XCUIElement* iconOldType =
|
||||||
|
[app.menuBars childrenMatchingType:XCUIElementTypeMenuBarItem].element;
|
||||||
|
if (![iconOldType waitForExistenceWithTimeout:5.0]) {
|
||||||
|
icon = iconOldType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the initial elements.
|
||||||
|
XCTAssert([app waitForExistenceWithTimeout:10.0]);
|
||||||
|
XCTAssert([icon waitForExistenceWithTimeout:10.0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) tearDown {
|
- (void) tearDown {
|
||||||
|
@ -91,10 +105,22 @@
|
||||||
- (void) testCycleOutputDevices {
|
- (void) testCycleOutputDevices {
|
||||||
const int NUM_CYCLES = 2;
|
const int NUM_CYCLES = 2;
|
||||||
|
|
||||||
// Get the list of output devices from the preferences menu.
|
// sbApp lets us use AppleScript to query BGMApp and check the test has made the changes to its
|
||||||
|
// settings we expect.
|
||||||
|
BGMAppApplication* sbApp = [SBApplication applicationWithBundleIdentifier:@kBGMAppBundleID];
|
||||||
|
|
||||||
|
// Get macOS to show the "'Xcode' wants to control 'Background Music'" dialog before we start
|
||||||
|
// the test so it doesn't interrupt it.
|
||||||
|
[[sbApp selectedOutputDevice] name];
|
||||||
|
|
||||||
|
// Click the icon to open the main menu.
|
||||||
[icon click];
|
[icon click];
|
||||||
[prefs hover];
|
|
||||||
NSArray<XCUIElement*>* outputDeviceMenuItems = [self outputDeviceMenuItems];
|
// Get the list of output devices from the main menu.
|
||||||
|
// BGMOutputDevicePrefs::createMenuItemForDevice gives every output device menu item the
|
||||||
|
// accessibility identifier "output-device" so we can find all of them here.
|
||||||
|
NSArray<XCUIElement*>* outputDeviceMenuItems =
|
||||||
|
[menuItems matchingIdentifier:@"output-device"].allElementsBoundByIndex;
|
||||||
|
|
||||||
// For debugging certain issues, it can be useful to repeatedly switch between two
|
// For debugging certain issues, it can be useful to repeatedly switch between two
|
||||||
// devices:
|
// devices:
|
||||||
|
@ -105,14 +131,10 @@
|
||||||
// Click the last device to close the menu again.
|
// Click the last device to close the menu again.
|
||||||
[outputDeviceMenuItems.lastObject click];
|
[outputDeviceMenuItems.lastObject click];
|
||||||
|
|
||||||
BGMAppApplication* sbApp = [SBApplication applicationWithBundleIdentifier:@kBGMAppBundleID];
|
|
||||||
|
|
||||||
for (int i = 0; i < NUM_CYCLES; i++) {
|
for (int i = 0; i < NUM_CYCLES; i++) {
|
||||||
// Select each output device.
|
// Select each output device.
|
||||||
for (XCUIElement* item in outputDeviceMenuItems) {
|
for (XCUIElement* item in outputDeviceMenuItems) {
|
||||||
[icon click];
|
[icon click];
|
||||||
[prefs hover];
|
|
||||||
|
|
||||||
[item click];
|
[item click];
|
||||||
|
|
||||||
// Assert that the device we clicked is the selected device now.
|
// Assert that the device we clicked is the selected device now.
|
||||||
|
@ -128,29 +150,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the menu items for the output devices in the preferences menu.
|
|
||||||
- (NSArray<XCUIElement*>*) outputDeviceMenuItems {
|
|
||||||
NSArray<XCUIElement*>* items = @[];
|
|
||||||
BOOL inOutputDeviceSection = NO;
|
|
||||||
|
|
||||||
for (int i = 0; i < prefs.menuItems.count; i++) {
|
|
||||||
XCUIElement* menuItem = [prefs.menuItems elementBoundByIndex:i];
|
|
||||||
|
|
||||||
if ([menuItem.title isEqual:@"Output Device"]) {
|
|
||||||
inOutputDeviceSection = YES;
|
|
||||||
} else if (inOutputDeviceSection) {
|
|
||||||
// Assume that finding a separator menu item means we've reached the end of the section.
|
|
||||||
if (((NSMenuItem*)menuItem.value).separatorItem || [menuItem.title isEqual:@""]) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
items = [items arrayByAddingObject:menuItem];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) testSelectMusicPlayer {
|
- (void) testSelectMusicPlayer {
|
||||||
// Select VLC as the music player.
|
// Select VLC as the music player.
|
||||||
[icon click];
|
[icon click];
|
||||||
|
|
Loading…
Reference in a new issue