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; };
|
||||
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>"; };
|
||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMOutputDevicePrefs.h; path = Preferences/BGMOutputDevicePrefs.h; sourceTree = "<group>"; };
|
||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMOutputDevicePrefs.mm; path = Preferences/BGMOutputDevicePrefs.mm; 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; 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>"; };
|
||||
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>"; };
|
||||
|
@ -419,8 +419,6 @@
|
|||
27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */,
|
||||
1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */,
|
||||
1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */,
|
||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */,
|
||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */,
|
||||
);
|
||||
name = "Preferences Menu";
|
||||
sourceTree = "<group>";
|
||||
|
@ -573,6 +571,8 @@
|
|||
1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */,
|
||||
1C4699451BD5BF2E00F78043 /* Music Players */,
|
||||
1C0BD0A21BF1A827004F4CF5 /* Preferences Menu */,
|
||||
1CE7064A1BF1EC0600BFC06D /* BGMOutputDevicePrefs.h */,
|
||||
1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */,
|
||||
1C46994D1BD7694C00F78043 /* BGMDeviceControlSync.h */,
|
||||
1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */,
|
||||
1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */,
|
||||
|
|
|
@ -25,17 +25,18 @@
|
|||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
#import "BGMAppVolumesController.h"
|
||||
#import "BGMAutoPauseMusic.h"
|
||||
#import "BGMAutoPauseMenuItem.h"
|
||||
#import "BGMSystemSoundsVolume.h"
|
||||
#import "BGMAppVolumesController.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
#import "BGMOutputDevicePrefs.h"
|
||||
#import "BGMOutputVolumeMenuItem.h"
|
||||
#import "BGMPreferencesMenu.h"
|
||||
#import "BGMPreferredOutputDevices.h"
|
||||
#import "BGMXPCListener.h"
|
||||
#import "BGMOutputVolumeMenuItem.h"
|
||||
#import "BGMSystemSoundsVolume.h"
|
||||
#import "BGMTermination.h"
|
||||
#import "BGMUserDefaults.h"
|
||||
#import "BGMXPCListener.h"
|
||||
#import "SystemPreferences.h"
|
||||
|
||||
// System Includes
|
||||
|
@ -61,6 +62,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
|||
BGMMusicPlayers* musicPlayers;
|
||||
BGMSystemSoundsVolume* systemSoundsVolume;
|
||||
BGMAppVolumesController* appVolumes;
|
||||
BGMOutputDevicePrefs* outputDevicePrefs;
|
||||
BGMPreferencesMenu* prefsMenu;
|
||||
BGMXPCListener* xpcListener;
|
||||
BGMPreferredOutputDevices* preferredOutputDevices;
|
||||
|
@ -292,9 +294,15 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
|||
|
||||
[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
|
||||
audioDevices:audioDevices
|
||||
preferredDevices:preferredOutputDevices
|
||||
musicPlayers:musicPlayers
|
||||
aboutPanel:self.aboutPanel
|
||||
aboutPanelLicenseView:self.aboutPanelLicenseView];
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
|
||||
// Forward Declarations
|
||||
@class BGMOutputVolumeMenuItem;
|
||||
@class BGMOutputDevicePrefs;
|
||||
|
||||
|
||||
#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.
|
||||
- (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
|
||||
- (NSError* __nullable) setBGMDeviceAsOSDefault;
|
||||
// Replace BGMDevice as the default device with the output device
|
||||
|
|
|
@ -26,16 +26,17 @@
|
|||
// Local Includes
|
||||
#import "BGM_Types.h"
|
||||
#import "BGM_Utils.h"
|
||||
#import "BGMDeviceControlSync.h"
|
||||
#import "BGMPlayThrough.h"
|
||||
#import "BGMAudioDevice.h"
|
||||
#import "BGMXPCProtocols.h"
|
||||
#import "BGMDeviceControlSync.h"
|
||||
#import "BGMOutputDevicePrefs.h"
|
||||
#import "BGMOutputVolumeMenuItem.h"
|
||||
#import "BGMPlayThrough.h"
|
||||
#import "BGMXPCProtocols.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
#import "CAAutoDisposer.h"
|
||||
#import "CAAtomic.h"
|
||||
#import "CAAutoDisposer.h"
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
@ -60,6 +61,7 @@
|
|||
NSXPCConnection* __nullable bgmXPCHelperConnection;
|
||||
|
||||
BGMOutputVolumeMenuItem* __nullable outputVolumeMenuItem;
|
||||
BGMOutputDevicePrefs* __nullable outputDevicePrefs;
|
||||
|
||||
NSRecursiveLock* stateLock;
|
||||
}
|
||||
|
@ -71,6 +73,7 @@
|
|||
stateLock = [NSRecursiveLock new];
|
||||
bgmXPCHelperConnection = nil;
|
||||
outputVolumeMenuItem = nil;
|
||||
outputDevicePrefs = nil;
|
||||
outputDevice = kAudioObjectUnknown;
|
||||
|
||||
try {
|
||||
|
@ -102,6 +105,10 @@
|
|||
outputVolumeMenuItem = item;
|
||||
}
|
||||
|
||||
- (void) setOutputDevicePrefs:(BGMOutputDevicePrefs*)prefs {
|
||||
outputDevicePrefs = prefs;
|
||||
}
|
||||
|
||||
#pragma mark Systemwide Default Device
|
||||
|
||||
// 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.
|
||||
[outputVolumeMenuItem outputDeviceDidChange];
|
||||
[outputDevicePrefs outputDeviceDidChange];
|
||||
}
|
||||
|
||||
- (NSError*) failedToSetOutputDevice:(AudioDeviceID)deviceID
|
||||
|
|
|
@ -32,9 +32,13 @@
|
|||
|
||||
@interface BGMOutputDevicePrefs : NSObject
|
||||
|
||||
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices;
|
||||
- (void) populatePreferencesMenu:(NSMenu*)prefsMenu;
|
||||
- (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
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
|
||||
|
|
@ -29,36 +29,111 @@
|
|||
#import "BGMAudioDevice.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#include "CAHALAudioSystemObject.h"
|
||||
#include "CAHALAudioDevice.h"
|
||||
#include "CAAutoDisposer.h"
|
||||
#import "CAAutoDisposer.h"
|
||||
#import "CAHALAudioDevice.h"
|
||||
#import "CAHALAudioSystemObject.h"
|
||||
#import "CAPropertyAddress.h"
|
||||
|
||||
// STL Includes
|
||||
#import <set>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
static NSInteger const kOutputDeviceMenuItemTag = 2;
|
||||
static NSInteger const kOutputDeviceMenuItemTag = 5;
|
||||
|
||||
@implementation BGMOutputDevicePrefs {
|
||||
NSMenu* bgmMenu;
|
||||
BGMAudioDeviceManager* audioDevices;
|
||||
BGMPreferredOutputDevices* preferredDevices;
|
||||
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
|
||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices {
|
||||
- (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices {
|
||||
if ((self = [super init])) {
|
||||
bgmMenu = inBGMMenu;
|
||||
audioDevices = inAudioDevices;
|
||||
preferredDevices = inPreferredDevices;
|
||||
outputDeviceMenuItems = [NSMutableArray new];
|
||||
|
||||
[self listenForDevicesAddedOrRemoved];
|
||||
[self populateBGMMenu];
|
||||
}
|
||||
|
||||
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
|
||||
for (NSMenuItem* item in outputDeviceMenuItems) {
|
||||
[prefsMenu removeItem:item];
|
||||
DebugMsg("BGMOutputDevicePrefs::populateBGMMenu: Removing %s", item.description.UTF8String);
|
||||
[bgmMenu removeItem:item];
|
||||
}
|
||||
|
||||
[outputDeviceMenuItems removeAllObjects];
|
||||
|
@ -72,25 +147,51 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
|||
audioSystem.GetAudioDevices(numDevices, devices);
|
||||
|
||||
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.
|
||||
const NSInteger menuItemsIdx = [prefsMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
|
||||
const NSInteger menuItemsIdx = [bgmMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
|
||||
|
||||
BOOL canBeOutputDevice = YES;
|
||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::insertMenuItemsForDevice", ([&] {
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
canBeOutputDevice = device.CanBeOutputDeviceInBGMApp();
|
||||
}));
|
||||
});
|
||||
|
||||
if (canBeOutputDevice) {
|
||||
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];
|
||||
}
|
||||
|
||||
// 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: Use the current data source's name when the control isn't settable, but only add one menu item.
|
||||
UInt32 numDataSources = 0;
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", [&] {
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
if (device.HasDataSourceControl(scope, channel) &&
|
||||
device.DataSourceControlIsSettable(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",
|
||||
"Device ID:", device.GetObjectID(),
|
||||
", Data source ID:", dataSourceIDs[i]);
|
||||
|
||||
BGMLogAndSwallowExceptionsMsg("BGMOutputDevicePrefs::createMenuItemsForDevice", "(DS)", [&]() {
|
||||
NSNumber* dataSourceID = [NSNumber numberWithUnsignedInt:dataSourceIDs[i]];
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, "(DS)", [&] {
|
||||
NSString* dataSourceName =
|
||||
CFBridgingRelease(device.CopyDataSourceNameForID(scope, channel, dataSourceIDs[i]));
|
||||
NSString* deviceName = CFBridgingRelease(device.CopyName());
|
||||
|
||||
[items addObject:[self createMenuItemForDevice:device
|
||||
dataSourceID:dataSourceID
|
||||
dataSourceID:@(dataSourceIDs[i])
|
||||
title:dataSourceName
|
||||
toolTip:deviceName]];
|
||||
});
|
||||
|
@ -142,13 +242,13 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
|||
} else {
|
||||
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u",
|
||||
"Device ID:", device.GetObjectID());
|
||||
|
||||
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", ([&] {
|
||||
|
||||
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
|
||||
[items addObject:[self createMenuItemForDevice:device
|
||||
dataSourceID:nil
|
||||
title:CFBridgingRelease(device.CopyName())
|
||||
toolTip:nil]];
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
|
@ -164,13 +264,13 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
|||
}
|
||||
|
||||
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:BGMNN(title)
|
||||
action:@selector(outputDeviceWasChanged:)
|
||||
action:@selector(outputDeviceMenuItemSelected:)
|
||||
keyEquivalent:@""];
|
||||
|
||||
// Add the AirPlay icon to the labels of AirPlay devices.
|
||||
//
|
||||
// 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) {
|
||||
item.image = [NSImage imageNamed:@"AirPlayIcon"];
|
||||
|
||||
|
@ -192,12 +292,28 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
|
|||
item.indentationLevel = 1;
|
||||
item.representedObject = @{ @"deviceID": @(device.GetObjectID()),
|
||||
@"dataSourceID": dataSourceID ? BGMNN(dataSourceID) : [NSNull null] };
|
||||
|
||||
if (@available(macOS 10.10, *)) {
|
||||
// Used for UI tests.
|
||||
item.accessibilityIdentifier = @"output-device";
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
- (void) outputDeviceWasChanged:(NSMenuItem*)menuItem {
|
||||
DebugMsg("BGMOutputDevicePrefs::outputDeviceWasChanged: '%s' menu item selected",
|
||||
// Called by BGMAudioDeviceManager to tell us a different device has been set as the output device.
|
||||
- (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]);
|
||||
|
||||
// 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] :
|
||||
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
|
||||
// DISPATCH_QUEUE_PRIORITY_HIGH, which is the second highest priority.)
|
||||
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
|
||||
newDataSource:newDataSourceID
|
||||
deviceName:deviceName];
|
|
@ -210,6 +210,14 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device";
|
|||
- (void) 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)) {
|
||||
outputDevice.RemovePropertyListenerBlock(
|
||||
CAPropertyAddress(kAudioDevicePropertyDataSource, kScope),
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?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>
|
||||
<deployment identifier="macosx"/>
|
||||
<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"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
|
@ -40,6 +40,10 @@
|
|||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<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">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Preferences" id="Img-Dh-cpU">
|
||||
|
@ -48,10 +52,6 @@
|
|||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<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">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
|
||||
// Local Includes
|
||||
#import "BGMAudioDeviceManager.h"
|
||||
#import "BGMPreferredOutputDevices.h"
|
||||
#import "BGMMusicPlayers.h"
|
||||
|
||||
// System Includes
|
||||
|
@ -34,11 +33,10 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BGMPreferencesMenu : NSObject <NSMenuDelegate>
|
||||
@interface BGMPreferencesMenu : NSObject
|
||||
|
||||
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices
|
||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
||||
aboutPanel:(NSPanel*)inAboutPanel
|
||||
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView;
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
|
||||
// Local Includes
|
||||
#import "BGMAutoPauseMusicPrefs.h"
|
||||
#import "BGMOutputDevicePrefs.h"
|
||||
#import "BGMAboutPanel.h"
|
||||
|
||||
|
||||
|
@ -38,7 +37,6 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
|
|||
@implementation BGMPreferencesMenu {
|
||||
// Menu sections
|
||||
BGMAutoPauseMusicPrefs* autoPauseMusicPrefs;
|
||||
BGMOutputDevicePrefs* outputDevicePrefs;
|
||||
|
||||
// The About Background Music window
|
||||
BGMAboutPanel* aboutPanel;
|
||||
|
@ -46,21 +44,16 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
|
|||
|
||||
- (id) initWithBGMMenu:(NSMenu*)inBGMMenu
|
||||
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
|
||||
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices
|
||||
musicPlayers:(BGMMusicPlayers*)inMusicPlayers
|
||||
aboutPanel:(NSPanel*)inAboutPanel
|
||||
aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView {
|
||||
if ((self = [super init])) {
|
||||
NSMenu* prefsMenu = [[inBGMMenu itemWithTag:kPreferencesMenuItemTag] submenu];
|
||||
[prefsMenu setDelegate:self];
|
||||
|
||||
autoPauseMusicPrefs = [[BGMAutoPauseMusicPrefs alloc] initWithPreferencesMenu:prefsMenu
|
||||
audioDevices:inAudioDevices
|
||||
musicPlayers:inMusicPlayers];
|
||||
|
||||
outputDevicePrefs = [[BGMOutputDevicePrefs alloc] initWithAudioDevices:inAudioDevices
|
||||
preferredDevices:inPreferredDevices];
|
||||
|
||||
aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView];
|
||||
|
||||
// Set up the "About Background Music" menu item
|
||||
|
@ -72,12 +65,6 @@ static NSInteger const kAboutPanelMenuItemTag = 3;
|
|||
return self;
|
||||
}
|
||||
|
||||
#pragma mark NSMenuDelegate
|
||||
|
||||
- (void) menuNeedsUpdate:(NSMenu*)menu {
|
||||
[outputDevicePrefs populatePreferencesMenu:menu];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
/*
|
||||
* BGMApp.h
|
||||
*
|
||||
* Generated with
|
||||
* sdef "/Applications/Background Music.app" | sdp -fh --basename BGMApp
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
@ -14,11 +17,11 @@
|
|||
* Background Music
|
||||
*/
|
||||
|
||||
// an output audio device
|
||||
// A hardware device that can play audio
|
||||
@interface BGMAppOutputDevice : SBObject
|
||||
|
||||
@property (copy, readonly) NSString *name;
|
||||
@property BOOL selected; // is this the device to be used for audio output?
|
||||
@property (copy, readonly) NSString *name; // The name of the output device.
|
||||
@property BOOL selected; // Is this the device to be used for audio output?
|
||||
|
||||
@end
|
||||
|
||||
|
@ -27,5 +30,7 @@
|
|||
|
||||
- (SBElementArray<BGMAppOutputDevice *> *) outputDevices;
|
||||
|
||||
@property (copy) BGMAppOutputDevice *selectedOutputDevice; // The device to be used for audio output
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMAppUITests.mm
|
||||
// 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.
|
||||
//
|
||||
|
@ -60,8 +60,8 @@
|
|||
// Set up the app object and some convenience vars.
|
||||
app = [[XCUIApplication alloc] init];
|
||||
menuItems = app.menuBars.menuItems;
|
||||
icon = [app.menuBars childrenMatchingType:XCUIElementTypeMenuBarItem].element;
|
||||
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.
|
||||
|
||||
|
@ -72,6 +72,20 @@
|
|||
|
||||
// Launch BGMApp.
|
||||
[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 {
|
||||
|
@ -91,10 +105,22 @@
|
|||
- (void) testCycleOutputDevices {
|
||||
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];
|
||||
[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
|
||||
// devices:
|
||||
|
@ -105,14 +131,10 @@
|
|||
// Click the last device to close the menu again.
|
||||
[outputDeviceMenuItems.lastObject click];
|
||||
|
||||
BGMAppApplication* sbApp = [SBApplication applicationWithBundleIdentifier:@kBGMAppBundleID];
|
||||
|
||||
for (int i = 0; i < NUM_CYCLES; i++) {
|
||||
// Select each output device.
|
||||
for (XCUIElement* item in outputDeviceMenuItems) {
|
||||
[icon click];
|
||||
[prefs hover];
|
||||
|
||||
[item click];
|
||||
|
||||
// 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 {
|
||||
// Select VLC as the music player.
|
||||
[icon click];
|
||||
|
|
Loading…
Reference in a new issue