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; };
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 */,

View file

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

View file

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

View file

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

View file

@ -32,9 +32,13 @@
@interface BGMOutputDevicePrefs : NSObject
- (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices
- (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu
audioDevices:(BGMAudioDeviceManager*)inAudioDevices
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

View file

@ -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
- (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);
});
};
}
}
@ -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.
UInt32 numDataSources = 0;
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", [&] {
BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] {
if (device.HasDataSourceControl(scope, channel) &&
device.DataSourceControlIsSettable(scope, channel)) {
numDataSources = device.GetNumberAvailableDataSources(scope, channel);
@ -127,14 +228,13 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
"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]];
});
@ -143,12 +243,12 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
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"];
@ -193,11 +293,27 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
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;
// 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];
}
// 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
newDataSource:newDataSourceID
deviceName:deviceName];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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