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:
Kyle Neideck 2018-11-04 12:30:43 +11:00
parent 94f13e747c
commit 5e12f9fc01
No known key found for this signature in database
GPG key ID: CAA8D9B8E39EC18C
12 changed files with 245 additions and 108 deletions

View file

@ -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 */,

View file

@ -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];

View file

@ -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

View file

@ -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

View file

@ -32,9 +32,13 @@
@interface BGMOutputDevicePrefs : NSObject @interface BGMOutputDevicePrefs : NSObject
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices - (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices; preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices;
- (void) populatePreferencesMenu:(NSMenu*)prefsMenu;
// 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

View file

@ -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
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices { 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);
});
};
} }
} }
@ -110,7 +211,7 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
// 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);
@ -127,14 +228,13 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
"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]];
}); });
@ -143,12 +243,12 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
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"];
@ -193,11 +293,27 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
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;
// 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) { if (changingDevice) {
// Add the new output device to the list of preferred devices. // Add the new output device to the list of preferred devices.
[preferredDevices userChangedOutputDeviceTo:newDeviceID]; [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), ^{
[self changeToOutputDevice:newDeviceID [self changeToOutputDevice:newDeviceID
newDataSource:newDataSourceID newDataSource:newDataSourceID
deviceName:deviceName]; deviceName:deviceName];

View file

@ -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),

View file

@ -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>

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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];