diff --git a/.gitignore b/.gitignore index c8aa7eb..1bd401c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ tags cmake-build-debug/ /Background-Music-*/ BGM.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +Images/*.aux +Images/*.log # Everything below is from https://github.com/github/gitignore/blob/master/Objective-C.gitignore diff --git a/BGMApp/BGMApp.xcodeproj/project.pbxproj b/BGMApp/BGMApp.xcodeproj/project.pbxproj index 839b877..9dc60ee 100644 --- a/BGMApp/BGMApp.xcodeproj/project.pbxproj +++ b/BGMApp/BGMApp.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; }; + 19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; }; + 19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; }; + 19FE7921FD1B6C037429ECA4 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; }; + 19FE7DFF63F69E77C53BF95E /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; }; + 19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; }; 1C0BD0A51BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusicPrefs.mm"; }; }; 1C0BD0A81BF1B029004F4CF5 /* BGMPreferencesMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPreferencesMenu.mm"; }; }; 1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusic.mm"; }; }; @@ -206,6 +212,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMVolumeChangeListener.cpp; sourceTree = ""; }; + 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMStatusBarItem.mm; sourceTree = ""; }; + 19FE799A86A285DD9423D164 /* BGMStatusBarItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMStatusBarItem.h; sourceTree = ""; }; + 19FE7FDAEBC3F0DB8C99823B /* BGMVolumeChangeListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMVolumeChangeListener.h; sourceTree = ""; }; 1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMAutoPauseMusicPrefs.h; path = Preferences/BGMAutoPauseMusicPrefs.h; sourceTree = ""; }; 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMAutoPauseMusicPrefs.mm; path = Preferences/BGMAutoPauseMusicPrefs.mm; sourceTree = ""; }; 1C0BD0A61BF1B029004F4CF5 /* BGMPreferencesMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMPreferencesMenu.h; path = Preferences/BGMPreferencesMenu.h; sourceTree = ""; }; @@ -579,10 +589,14 @@ 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */, 1C1962E61BC94E91008A4DF7 /* BGMPlayThrough.h */, 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */, + 19FE799A86A285DD9423D164 /* BGMStatusBarItem.h */, + 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */, 1CC6593B1F91DEB400B0CCDC /* BGMTermination.h */, 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */, 2743C9ED1D8538700089613B /* BGMUserDefaults.h */, 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */, + 19FE7FDAEBC3F0DB8C99823B /* BGMVolumeChangeListener.h */, + 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */, 2795973C1C982E8C00A002FB /* BGMXPCListener.h */, 2795973A1C982E4E00A002FB /* BGMXPCListener.mm */, 1C2FC3161EC7078F00A76592 /* Scripting */, @@ -1004,6 +1018,8 @@ 2795973B1C982E4E00A002FB /* BGMXPCListener.mm in Sources */, 27C457E61CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m in Sources */, 1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */, + 19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */, + 19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1061,6 +1077,8 @@ 1CCC4F621E584100008053E4 /* BGMAppUITests.mm in Sources */, 1C2FC31C1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */, 1C2FC3151EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */, + 19FE7921FD1B6C037429ECA4 /* BGMStatusBarItem.mm in Sources */, + 19FE7DFF63F69E77C53BF95E /* BGMVolumeChangeListener.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1134,6 +1152,8 @@ 1CC6593E1F91DEB400B0CCDC /* BGMTermination.mm in Sources */, 2743CA011D86D3CB0089613B /* BGMMusicPlayers.mm in Sources */, 2743CA021D86D3CB0089613B /* BGMiTunes.m in Sources */, + 19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */, + 19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BGMApp/BGMApp/BGMAppDelegate.mm b/BGMApp/BGMApp/BGMAppDelegate.mm index a12b0bf..a2bea4f 100644 --- a/BGMApp/BGMApp/BGMAppDelegate.mm +++ b/BGMApp/BGMApp/BGMAppDelegate.mm @@ -17,10 +17,10 @@ // BGMAppDelegate.mm // BGMApp // -// Copyright © 2016-2018 Kyle Neideck +// Copyright © 2016-2019 Kyle Neideck // -// Self Includes +// Self Include #import "BGMAppDelegate.h" // Local Includes @@ -33,6 +33,7 @@ #import "BGMOutputVolumeMenuItem.h" #import "BGMPreferencesMenu.h" #import "BGMPreferredOutputDevices.h" +#import "BGMStatusBarItem.h" #import "BGMSystemSoundsVolume.h" #import "BGMTermination.h" #import "BGMUserDefaults.h" @@ -45,18 +46,19 @@ #pragma clang assume_nonnull begin -static float const kStatusBarIconPadding = 0.25; static NSString* const kOptNoPersistentData = @"--no-persistent-data"; static NSString* const kOptShowDockIcon = @"--show-dock-icon"; @implementation BGMAppDelegate { - // The button in the system status bar (the bar with volume, battery, clock, etc.) to show the main menu - // for the app. These are called "menu bar extras" in the Human Interface Guidelines. - NSStatusItem* statusBarItem; + // The button in the system status bar that shows the main menu. + BGMStatusBarItem* statusBarItem; // Only show the 'BGMXPCHelper is missing' error dialog once. BOOL haveShownXPCHelperErrorMessage; - + + // Persistently stores user settings and data. + BGMUserDefaults* userDefaults; + BGMAutoPauseMusic* autoPauseMusic; BGMAutoPauseMenuItem* autoPauseMenuItem; BGMMusicPlayers* musicPlayers; @@ -71,6 +73,8 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon"; @synthesize audioDevices = audioDevices; - (void) awakeFromNib { + [super awakeFromNib]; + // Show BGMApp in the dock, if the command-line option for that was passed. This is used by the // UI tests. if ([NSProcessInfo.processInfo.arguments indexOfObject:kOptShowDockIcon] != NSNotFound) { @@ -79,72 +83,19 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon"; haveShownXPCHelperErrorMessage = NO; - [self initStatusBarItem]; -} - -// Set up the status bar item. (The thing you click to show BGMApp's UI.) -- (void) initStatusBarItem { - statusBarItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; - - // NSStatusItem doesn't have the "button" property on OS X 10.9. - BOOL buttonAvailable = (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_10); - - // Set the title/tooltip to "Background Music". - statusBarItem.title = [NSRunningApplication currentApplication].localizedName; - statusBarItem.toolTip = statusBarItem.title; - - if (buttonAvailable) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpartial-availability" - statusBarItem.button.accessibilityLabel = statusBarItem.title; -#pragma clang diagnostic pop + // Set up audioDevices, which coordinates BGMDevice and the output device. It manages + // playthrough, volume/mute controls, etc. + if (![self initAudioDeviceManager]) { + return; } - // Set the icon. - NSImage* icon = [NSImage imageNamed:@"FermataIcon"]; + // Stored user settings + userDefaults = [self createUserDefaults]; - if (icon != nil) { - NSRect statusBarItemFrame; - - if (buttonAvailable) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpartial-availability" - statusBarItemFrame = statusBarItem.button.frame; -#pragma clang diagnostic pop - } else { - // OS X 10.9 fallback. I haven't tested this (or anything else on 10.9). - statusBarItemFrame = statusBarItem.view.frame; - } - - CGFloat lengthMinusPadding = statusBarItemFrame.size.height * (1 - kStatusBarIconPadding); - [icon setSize:NSMakeSize(lengthMinusPadding, lengthMinusPadding)]; - - // Make the icon a "template image" so it gets drawn colour-inverted when it's highlighted or the status - // bar's in dark mode - [icon setTemplate:YES]; - - if (buttonAvailable) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpartial-availability" - statusBarItem.button.image = icon; -#pragma clang diagnostic pop - } else { - statusBarItem.image = icon; - } - } else { - // If our icon is missing for some reason, fallback to a fermata character (1D110) - if (buttonAvailable) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpartial-availability" - statusBarItem.button.title = @"𝄐"; -#pragma clang diagnostic pop - } else { - statusBarItem.title = @"𝄐"; - } - } - - // Set the main menu - statusBarItem.menu = self.bgmMenu; + // Add the status bar item. (The thing you click to show BGMApp's main menu.) + statusBarItem = [[BGMStatusBarItem alloc] initWithMenu:self.bgmMenu + audioDevices:audioDevices + userDefaults:userDefaults]; } - (void) applicationDidFinishLaunching:(NSNotification*)aNotification { @@ -159,15 +110,6 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon"; NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"], NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]); - // Set up audioDevices, which coordinates BGMDevice and the output device. It manages - // playthrough, volume/mute controls, etc. - if (![self initAudioDeviceManager]) { - return; - } - - // Persistently stores user settings and data. - BGMUserDefaults* userDefaults = [self createUserDefaults]; - // Handles changing (or not changing) the output device when devices are added or removed. Must // be initialised before calling setBGMDeviceAsDefault. preferredOutputDevices = @@ -191,7 +133,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon"; autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices musicPlayers:musicPlayers]; - [self setUpMainMenu:userDefaults]; + [self setUpMainMenu]; xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices helperConnectionErrorHandler:^(NSError* error) { @@ -285,7 +227,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon"; } } -- (void) setUpMainMenu:(BGMUserDefaults*)userDefaults { +- (void) setUpMainMenu { autoPauseMenuItem = [[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped autoPauseMusic:autoPauseMusic @@ -305,6 +247,7 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon"; prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu audioDevices:audioDevices musicPlayers:musicPlayers + statusBarItem:statusBarItem aboutPanel:self.aboutPanel aboutPanelLicenseView:self.aboutPanelLicenseView]; diff --git a/BGMApp/BGMApp/BGMOutputDeviceMenuSection.mm b/BGMApp/BGMApp/BGMOutputDeviceMenuSection.mm index a476236..275608d 100644 --- a/BGMApp/BGMApp/BGMOutputDeviceMenuSection.mm +++ b/BGMApp/BGMApp/BGMOutputDeviceMenuSection.mm @@ -202,7 +202,7 @@ static NSInteger const kOutputDeviceMenuItemTag = 5; NSMutableArray* items = [NSMutableArray new]; AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput; - UInt32 channel = 0; // 0 is the master channel. + UInt32 channel = kAudioObjectPropertyElementMaster; // If the device has data sources, create a menu item for each. Otherwise, create a single menu item // for the device. This way the menu items' titles will be, for example, "Internal Speakers" rather diff --git a/BGMApp/BGMApp/BGMOutputVolumeMenuItem.mm b/BGMApp/BGMApp/BGMOutputVolumeMenuItem.mm index 21a2d2a..082921a 100644 --- a/BGMApp/BGMApp/BGMOutputVolumeMenuItem.mm +++ b/BGMApp/BGMApp/BGMOutputVolumeMenuItem.mm @@ -17,7 +17,7 @@ // BGMOutputVolumeMenuItem.mm // BGMApp // -// Copyright © 2017, 2018 Kyle Neideck +// Copyright © 2017-2019 Kyle Neideck // // Self Include @@ -26,6 +26,7 @@ // Local Includes #import "BGM_Utils.h" #import "BGMAudioDevice.h" +#import "BGMVolumeChangeListener.h" // PublicUtility Includes #import "CAException.h" @@ -46,7 +47,7 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device"; NSTextField* deviceLabel; NSSlider* volumeSlider; BGMAudioDevice outputDevice; - AudioObjectPropertyListenerBlock updateSliderListenerBlock; + BGMVolumeChangeListener* volumeChangeListener; AudioObjectPropertyListenerBlock updateLabelListenerBlock; } @@ -66,9 +67,8 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device"; volumeSlider = slider; outputDevice = audioDevices.outputDevice; - // These are initialised in the methods called below. - updateSliderListenerBlock = nil; - updateLabelListenerBlock = nil; + // volumeChangeListener and updateLabelListenerBlock are initialised in the methods called + // below. // Apply our custom view from MainMenu.xib. self.view = view; @@ -89,20 +89,6 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device"; // TODO: This call isn't thread safe. (But currently this dealloc method is only called if // there's an error.) [self removeOutputDeviceDataSourceListener]; - - BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] { - audioDevices.bgmDevice.RemovePropertyListenerBlock( - CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kScope), - dispatch_get_main_queue(), - updateSliderListenerBlock); - })); - - BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::dealloc", ([&] { - audioDevices.bgmDevice.RemovePropertyListenerBlock( - CAPropertyAddress(kAudioDevicePropertyMute, kScope), - dispatch_get_main_queue(), - updateSliderListenerBlock); - })); } - (void) initSlider { @@ -118,32 +104,9 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device"; // Register a listener that will update the slider when the user changes the volume or // mutes/unmutes their audio. BGMOutputVolumeMenuItem* __weak weakSelf = self; - - updateSliderListenerBlock = - ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) { - // The docs for AudioObjectPropertyListenerBlock say inAddresses will always contain - // at least one property the block is listening to, so there's no need to check it. - #pragma unused (inNumberAddresses, inAddresses) - [weakSelf updateVolumeSlider]; - }; - - // Instead of swallowing exceptions, we could try again later, but I doubt it would be worth the - // effort. And the documentation doesn't actually explain what could cause this to fail. - BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::initSlider", ([&] { - // Register the listener to receive volume notifications. - audioDevices.bgmDevice.AddPropertyListenerBlock( - CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kScope), - dispatch_get_main_queue(), - updateSliderListenerBlock); - })); - - BGMLogAndSwallowExceptions("BGMOutputVolumeMenuItem::initSlider", ([&] { - // Register the same listener for mute/unmute notifications. - audioDevices.bgmDevice.AddPropertyListenerBlock( - CAPropertyAddress(kAudioDevicePropertyMute, kScope), - dispatch_get_main_queue(), - updateSliderListenerBlock); - })); + volumeChangeListener = new BGMVolumeChangeListener(audioDevices.bgmDevice, [&] { + [weakSelf updateVolumeSlider]; + }); } // Updates the value of the output volume slider. Should only be called on the main thread because @@ -178,7 +141,7 @@ NSString* const __nonnull kGenericOutputDeviceName = @"Output Device"; volumeSlider.doubleValue = 0.0; } })); -}; +} - (void) addOutputDeviceDataSourceListener { // Create the block that updates deviceLabel when the output device's data source changes, e.g. diff --git a/BGMApp/BGMApp/BGMStatusBarItem.h b/BGMApp/BGMApp/BGMStatusBarItem.h new file mode 100644 index 0000000..d26f35f --- /dev/null +++ b/BGMApp/BGMApp/BGMStatusBarItem.h @@ -0,0 +1,63 @@ +// This file is part of Background Music. +// +// Background Music is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 2 of the +// License, or (at your option) any later version. +// +// Background Music is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Background Music. If not, see . + +// +// BGMStatusBarItem.h +// BGMApp +// +// Copyright © 2019 Kyle Neideck +// +// The button in the system status bar (the bar with volume, battery, clock, etc.) to show the main +// menu for the app. These are called "menu bar extras" in the Human Interface Guidelines. +// + +// Local Includes +#import "BGMAudioDeviceManager.h" + +// System Includes +#import + +// Forward Declarations +@class BGMUserDefaults; + + +#pragma clang assume_nonnull begin + +typedef NS_ENUM(NSInteger, BGMStatusBarIcon) { + BGMFermataStatusBarIcon = 0, + BGMVolumeStatusBarIcon +}; + +static BGMStatusBarIcon const kBGMStatusBarIconMinValue = BGMFermataStatusBarIcon; +static BGMStatusBarIcon const kBGMStatusBarIconMaxValue = BGMVolumeStatusBarIcon; +static BGMStatusBarIcon const kBGMStatusBarIconDefaultValue = BGMFermataStatusBarIcon; + +@interface BGMStatusBarItem : NSObject + +- (instancetype) initWithMenu:(NSMenu*)bgmMenu + audioDevices:(BGMAudioDeviceManager*)devices + userDefaults:(BGMUserDefaults*)defaults; + +// Set this to BGMFermataStatusBarIcon to change the icon to the Background Music logo. +// +// Set this to BGMFermataStatusBarIcon to change the icon to a volume icon. This icon has the +// advantage of indicating the volume level, but we can't make it the default because it looks the +// same as the icon for the macOS volume status bar item. +@property BGMStatusBarIcon icon; + +@end + +#pragma clang assume_nonnull end + diff --git a/BGMApp/BGMApp/BGMStatusBarItem.mm b/BGMApp/BGMApp/BGMStatusBarItem.mm new file mode 100644 index 0000000..2d637fe --- /dev/null +++ b/BGMApp/BGMApp/BGMStatusBarItem.mm @@ -0,0 +1,257 @@ +// This file is part of Background Music. +// +// Background Music is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 2 of the +// License, or (at your option) any later version. +// +// Background Music is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Background Music. If not, see . + +// +// BGMStatusBarItem.m +// BGMApp +// +// Copyright © 2019 Kyle Neideck +// + +// Self Include +#import "BGMStatusBarItem.h" + +// Local Includes +#import "BGM_Utils.h" +#import "BGMUserDefaults.h" +#import "BGMVolumeChangeListener.h" + + +#pragma clang assume_nonnull begin + +static CGFloat const kStatusBarIconPadding = 0.25; +static CGFloat const kVolumeIconAdditionalVerticalPadding = 0.075; + +@implementation BGMStatusBarItem +{ + BGMAudioDeviceManager* audioDevices; + + // User settings and data. + BGMUserDefaults* userDefaults; + + NSImage* fermataIcon; + NSImage* volumeIcon0SoundWaves; + NSImage* volumeIcon1SoundWave; + NSImage* volumeIcon2SoundWaves; + NSImage* volumeIcon3SoundWaves; + + NSStatusItem* statusBarItem; + BGMVolumeChangeListener* volumeChangeListener; + + BGMStatusBarIcon _icon; +} + +- (instancetype) initWithMenu:(NSMenu*)bgmMenu + audioDevices:(BGMAudioDeviceManager*)devices + userDefaults:(BGMUserDefaults*)defaults { + if ((self = [super init])) { + statusBarItem = + [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; + + audioDevices = devices; + userDefaults = defaults; + + // Initialise the icons. + [self initIcons]; + + // Set the initial icon. + self.icon = userDefaults.statusBarIcon; + + // Set the menu item to open the main menu. + statusBarItem.menu = bgmMenu; + + // Set the accessibility label to "Background Music". (We intentionally don't set a title or + // a tooltip.) + if ([BGMStatusBarItem buttonAvailable]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + statusBarItem.button.accessibilityLabel = + [NSRunningApplication currentApplication].localizedName; +#pragma clang diagnostic pop + } + + // Update the icon when BGMDevice's volume changes. + BGMStatusBarItem* __weak weakSelf = self; + volumeChangeListener = new BGMVolumeChangeListener(audioDevices.bgmDevice, [&] { + [weakSelf bgmDeviceVolumeDidChange]; + }); + } + + return self; +} + +- (void) dealloc { + delete volumeChangeListener; +} + +- (void) initIcons { + // Load the icons. + fermataIcon = [NSImage imageNamed:@"FermataIcon"]; + volumeIcon0SoundWaves = [NSImage imageNamed:@"Volume0"]; + volumeIcon1SoundWave = [NSImage imageNamed:@"Volume1"]; + volumeIcon2SoundWaves = [NSImage imageNamed:@"Volume2"]; + volumeIcon3SoundWaves = [NSImage imageNamed:@"Volume3"]; + + // Set the icons' sizes. + NSRect statusBarItemFrame; + + if ([BGMStatusBarItem buttonAvailable]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + statusBarItemFrame = statusBarItem.button.frame; +#pragma clang diagnostic pop + } else { + // OS X 10.9 fallback. I haven't tested this (or anything else on 10.9). + statusBarItemFrame = statusBarItem.view.frame; + } + + CGFloat heightMinusPadding = statusBarItemFrame.size.height * (1 - kStatusBarIconPadding); + + // The fermata icon has equal width and height. + [fermataIcon setSize:NSMakeSize(heightMinusPadding, heightMinusPadding)]; + + // The volume icons are all the same width and height. + CGFloat volumeIconWidthToHeightRatio = + volumeIcon0SoundWaves.size.width / volumeIcon0SoundWaves.size.height; + CGFloat volumeIconWidth = heightMinusPadding * volumeIconWidthToHeightRatio; + CGFloat volumeIconHeight = heightMinusPadding * (1 - kVolumeIconAdditionalVerticalPadding); + + [volumeIcon0SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)]; + [volumeIcon1SoundWave setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)]; + [volumeIcon2SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)]; + [volumeIcon3SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)]; + + // Make the icons "template images" so they get drawn colour-inverted when they're highlighted + // or the system is in dark mode. + [fermataIcon setTemplate:YES]; + [volumeIcon0SoundWaves setTemplate:YES]; + [volumeIcon1SoundWave setTemplate:YES]; + [volumeIcon2SoundWaves setTemplate:YES]; + [volumeIcon3SoundWaves setTemplate:YES]; +} + ++ (BOOL) buttonAvailable { + // NSStatusItem doesn't have the "button" property on OS X 10.9. + return (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_10); +} + +- (void) setImage:(NSImage*)image { + if ([BGMStatusBarItem buttonAvailable]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + statusBarItem.button.image = image; +#pragma clang diagnostic pop + } else { + statusBarItem.image = image; + } +} + +- (BGMStatusBarIcon) icon { + return _icon; +} + +- (void) setIcon:(BGMStatusBarIcon)icon { + _icon = icon; + + // Save the setting. + userDefaults.statusBarIcon = self.icon; + + // Change the icon (i.e. the image). Dispatch this to the main thread because it changes the UI. + dispatch_async(dispatch_get_main_queue(), ^{ + if (_icon == BGMFermataStatusBarIcon) { + [self setImage:fermataIcon]; + + // If the icon was greyed out, change it back. + if ([BGMStatusBarItem buttonAvailable]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + statusBarItem.button.appearsDisabled = NO; +#pragma clang diagnostic pop + } + } else { + BGMAssert((_icon == BGMVolumeStatusBarIcon), "Unknown icon in enum"); + + [self updateVolumeStatusBarIcon]; + } + }); +} + +- (void) bgmDeviceVolumeDidChange { + if (self.icon == BGMVolumeStatusBarIcon) { + [self updateVolumeStatusBarIcon]; + } +} + +// Should only be called on the main thread because it calls UI functions. +- (void) updateVolumeStatusBarIcon { + BGMAssert([[NSThread currentThread] isMainThread], + "updateVolumeStatusBarIcon called on non-main thread."); + BGMAssert((self.icon == BGMVolumeStatusBarIcon), "Volume status bar icon not enabled"); + + BGMAudioDevice bgmDevice = [audioDevices bgmDevice]; + + // BGMDevice should never return an error for these calls, so we just swallow any exceptions and + // give up. + BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { + AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput; + AudioObjectPropertyScope element = kAudioObjectPropertyElementMaster; + + BOOL hasVolume = bgmDevice.HasVolumeControl(scope, element); + + // Show the button as greyed out if BGMDevice doesn't have a volume control (which means the + // output device doesn't have one). + if ([BGMStatusBarItem buttonAvailable]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + statusBarItem.button.appearsDisabled = !hasVolume; +#pragma clang diagnostic pop + } + + if (hasVolume) { + if (bgmDevice.HasMuteControl(scope, element) && + bgmDevice.GetMuteControlValue(scope, element)) { + // The device is muted, so use the zero waves icon. + [self setImage:volumeIcon0SoundWaves]; + } else { + // Set the icon to reflect the device's volume. + double volume = bgmDevice.GetVolumeControlScalarValue(scope, element); + + // These values match the macOS volume status bar item, except for the first one. I + // don't know why, but at a very low volume macOS will show the zero waves icon even + // though the sound is still audible. + if (volume == 0.05) { + [self setImage:volumeIcon0SoundWaves]; + } else if (volume < 0.33) { + [self setImage:volumeIcon1SoundWave]; + } else if (volume < 0.66) { + [self setImage:volumeIcon2SoundWaves]; + } else { + [self setImage:volumeIcon3SoundWaves]; + } + } + } else { + // Always use the full-volume icon when the device has no volume control. + [self setImage:volumeIcon3SoundWaves]; + } + }); + + DebugMsg("BGMStatusBarItem::updateVolumeStatusBarIcon: Set icon to %s", + statusBarItem.image.name.UTF8String); +} + +@end + +#pragma clang assume_nonnull end + diff --git a/BGMApp/BGMApp/BGMUserDefaults.h b/BGMApp/BGMApp/BGMUserDefaults.h index 64300db..f902cfd 100644 --- a/BGMApp/BGMApp/BGMUserDefaults.h +++ b/BGMApp/BGMApp/BGMUserDefaults.h @@ -17,13 +17,16 @@ // BGMUserDefaults.h // BGMApp // -// Copyright © 2016-2018 Kyle Neideck +// Copyright © 2016-2019 Kyle Neideck // // A simple wrapper around our use of NSUserDefaults. Used to store the preferences/state that only // apply to BGMApp. The others are stored by BGMDriver. // -// System includes +// Local Includes +#import "BGMStatusBarItem.h" + +// System Includes #import @@ -44,6 +47,10 @@ // device is at index 0. See BGMPreferredOutputDevices. @property NSArray* preferredDeviceUIDs; +// The (type of) icon to show in the button in the status bar. (The button the user clicks to open +// BGMApp's main menu.) +@property BGMStatusBarIcon statusBarIcon; + @end #pragma clang assume_nonnull end diff --git a/BGMApp/BGMApp/BGMUserDefaults.m b/BGMApp/BGMApp/BGMUserDefaults.m index ec2a5f6..74b46d1 100644 --- a/BGMApp/BGMApp/BGMUserDefaults.m +++ b/BGMApp/BGMApp/BGMUserDefaults.m @@ -17,7 +17,7 @@ // BGMUserDefaults.m // BGMApp // -// Copyright © 2016-2018 Kyle Neideck +// Copyright © 2016-2019 Kyle Neideck // // Self Include @@ -32,7 +32,8 @@ // Keys static NSString* const BGMDefaults_AutoPauseMusicEnabled = @"AutoPauseMusicEnabled"; static NSString* const BGMDefaults_SelectedMusicPlayerID = @"SelectedMusicPlayerID"; -static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs"; +static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs"; +static NSString* const BGMDefaults_StatusBarIcon = @"StatusBarIcon"; @implementation BGMUserDefaults { // The defaults object wrapped by this object. @@ -89,6 +90,22 @@ static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs"; [self set:BGMDefaults_PreferredDeviceUIDs to:devices]; } +- (BGMStatusBarIcon) statusBarIcon { + NSInteger icon = [self getInt:BGMDefaults_StatusBarIcon or:kBGMStatusBarIconDefaultValue]; + + // Just in case we get an invalid value somehow. + if ((icon < kBGMStatusBarIconMinValue) || (icon > kBGMStatusBarIconMaxValue)) { + NSLog(@"BGMUserDefaults::statusBarIcon: Unknown BGMStatusBarIcon: %ld", (long)icon); + icon = kBGMStatusBarIconDefaultValue; + } + + return (BGMStatusBarIcon)icon; +} + +- (void) setStatusBarIcon:(BGMStatusBarIcon)icon { + [self setInt:BGMDefaults_StatusBarIcon to:icon]; +} + #pragma mark Implementation - (id __nullable) get:(NSString*)key { @@ -103,6 +120,7 @@ static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs"; } } +// TODO: This method should have a default value param. - (BOOL) getBool:(NSString*)key { return defaults ? [defaults boolForKey:key] : [transientDefaults[key] boolValue]; } @@ -111,7 +129,32 @@ static NSString* const BGMDefaults_PreferredDeviceUIDs = @"PreferredDeviceUIDs"; if (defaults) { [defaults setBool:value forKey:key]; } else { - transientDefaults[key] = [NSNumber numberWithBool:value]; + transientDefaults[key] = @(value); + } +} + +- (NSInteger) getInt:(NSString*)key or:(NSInteger)valueIfNil +{ + if (defaults) { + if ([defaults objectForKey:key]) { + return [defaults integerForKey:key]; + } else { + return valueIfNil; + } + } else { + if (transientDefaults[key]) { + return [transientDefaults[key] intValue]; + } else { + return valueIfNil; + } + } +} + +- (void) setInt:(NSString*)key to:(NSInteger)value { + if (defaults) { + [defaults setInteger:value forKey:key]; + } else { + transientDefaults[key] = @(value); } } diff --git a/BGMApp/BGMApp/BGMVolumeChangeListener.cpp b/BGMApp/BGMApp/BGMVolumeChangeListener.cpp new file mode 100644 index 0000000..95f6cfa --- /dev/null +++ b/BGMApp/BGMApp/BGMVolumeChangeListener.cpp @@ -0,0 +1,100 @@ +// This file is part of Background Music. +// +// Background Music is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 2 of the +// License, or (at your option) any later version. +// +// Background Music is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Background Music. If not, see . + +// +// BGMVolumeChangeListener.cpp +// BGMApp +// +// Copyright © 2019 Kyle Neideck +// + +// Self Include +#include "BGMVolumeChangeListener.h" + +// Local Includes +#import "BGM_Utils.h" +#import "BGMAudioDevice.h" + +// PublicUtility Includes +#import "CAException.h" +#import "CAPropertyAddress.h" + + +#pragma clang assume_nonnull begin + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wexit-time-destructors" +const static std::vector kVolumeChangeProperties = { + // Output volume changes + CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kAudioObjectPropertyScopeOutput), + // Mute/unmute + CAPropertyAddress(kAudioDevicePropertyMute, kAudioObjectPropertyScopeOutput), + // Received when controls are added to or removed from the device. + CAPropertyAddress(kAudioObjectPropertyControlList), + // Received when the device has changed and "clients should re-evaluate everything they need + // to know about the device, particularly the layout and values of the controls". + CAPropertyAddress(kAudioDevicePropertyDeviceHasChanged) +}; +#pragma clang diagnostic pop + +BGMVolumeChangeListener::BGMVolumeChangeListener(BGMAudioDevice device, + std::function handler) +: + mDevice(device) +{ + // Register a listener that will update the slider when the user changes the volume or + // mutes/unmutes their audio. + mListenerBlock = + Block_copy(^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) { + // The docs for AudioObjectPropertyListenerBlock say inAddresses will always contain + // at least one property the block is listening to, so there's no need to check it. + (void)inNumberAddresses; + (void)inAddresses; + + // Call the callback. + handler(); + }); + + // Register for a number of properties that might indicate that clients need to update. For + // example, the mute property changing means UI elements that display the volume will need to be + // updated, even though it's not strictly a change in volume. + for(CAPropertyAddress property : kVolumeChangeProperties) + { + // Instead of swallowing exceptions here, we could try again later, but I doubt it would be + // worth the effort. And the documentation doesn't actually explain what could cause this + // call to fail. + BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { + mDevice.AddPropertyListenerBlock(property, dispatch_get_main_queue(), mListenerBlock); + }); + } +} + +BGMVolumeChangeListener::~BGMVolumeChangeListener() +{ + // Deregister and release the listener block. + for(CAPropertyAddress property : kVolumeChangeProperties) + { + BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { + mDevice.RemovePropertyListenerBlock(property, + dispatch_get_main_queue(), + mListenerBlock); + }); + } + + Block_release(mListenerBlock); +} + +#pragma clang assume_nonnull end + diff --git a/BGMApp/BGMApp/BGMVolumeChangeListener.h b/BGMApp/BGMApp/BGMVolumeChangeListener.h new file mode 100644 index 0000000..4035889 --- /dev/null +++ b/BGMApp/BGMApp/BGMVolumeChangeListener.h @@ -0,0 +1,59 @@ +// This file is part of Background Music. +// +// Background Music is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 2 of the +// License, or (at your option) any later version. +// +// Background Music is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Background Music. If not, see . + +// +// BGMVolumeChangeListener.h +// BGMApp +// +// Copyright © 2019 Kyle Neideck +// + +// Local Includes +#include "BGMBackgroundMusicDevice.h" + +// PublicUtility Includes +#import "CAPropertyAddress.h" + +// STL Includes +#include + +// System Includes +#include + + +#pragma clang assume_nonnull begin + +class BGMVolumeChangeListener +{ + +public: + /*! + * @param device Listens for notifications about this device. + * @param handler The function to call when the device's volume (or mute) changes. Called on the + * main queue. + */ + BGMVolumeChangeListener(BGMAudioDevice device, std::function handler); + virtual ~BGMVolumeChangeListener(); + BGMVolumeChangeListener(const BGMVolumeChangeListener&) = delete; + BGMVolumeChangeListener& operator=(const BGMVolumeChangeListener&) = delete; + +private: + AudioObjectPropertyListenerBlock mListenerBlock; + BGMAudioDevice mDevice; + +}; + +#pragma clang assume_nonnull end + diff --git a/BGMApp/BGMApp/Base.lproj/MainMenu.xib b/BGMApp/BGMApp/Base.lproj/MainMenu.xib index 5bd6abd..5fa5e45 100644 --- a/BGMApp/BGMApp/Base.lproj/MainMenu.xib +++ b/BGMApp/BGMApp/Base.lproj/MainMenu.xib @@ -52,7 +52,17 @@ - + + + + + + + + + + + @@ -205,7 +215,7 @@ - + @@ -316,7 +326,7 @@ - + YnBsaXN0MDDUAQIDBAUGPT5YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK4HCBMU diff --git a/BGMApp/BGMApp/Images.xcassets/Volume0.imageset/Contents.json b/BGMApp/BGMApp/Images.xcassets/Volume0.imageset/Contents.json new file mode 100644 index 0000000..fbe18d7 --- /dev/null +++ b/BGMApp/BGMApp/Images.xcassets/Volume0.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "mac", + "filename" : "Volume0.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/BGMApp/BGMApp/Images.xcassets/Volume0.imageset/Volume0.pdf b/BGMApp/BGMApp/Images.xcassets/Volume0.imageset/Volume0.pdf new file mode 100644 index 0000000..f9c9a27 Binary files /dev/null and b/BGMApp/BGMApp/Images.xcassets/Volume0.imageset/Volume0.pdf differ diff --git a/BGMApp/BGMApp/Images.xcassets/Volume1.imageset/Contents.json b/BGMApp/BGMApp/Images.xcassets/Volume1.imageset/Contents.json new file mode 100644 index 0000000..5d78782 --- /dev/null +++ b/BGMApp/BGMApp/Images.xcassets/Volume1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "mac", + "filename" : "Volume1.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/BGMApp/BGMApp/Images.xcassets/Volume1.imageset/Volume1.pdf b/BGMApp/BGMApp/Images.xcassets/Volume1.imageset/Volume1.pdf new file mode 100644 index 0000000..acc1966 Binary files /dev/null and b/BGMApp/BGMApp/Images.xcassets/Volume1.imageset/Volume1.pdf differ diff --git a/BGMApp/BGMApp/Images.xcassets/Volume2.imageset/Contents.json b/BGMApp/BGMApp/Images.xcassets/Volume2.imageset/Contents.json new file mode 100644 index 0000000..b0da7cd --- /dev/null +++ b/BGMApp/BGMApp/Images.xcassets/Volume2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "mac", + "filename" : "Volume2.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/BGMApp/BGMApp/Images.xcassets/Volume2.imageset/Volume2.pdf b/BGMApp/BGMApp/Images.xcassets/Volume2.imageset/Volume2.pdf new file mode 100644 index 0000000..a1ee1b0 Binary files /dev/null and b/BGMApp/BGMApp/Images.xcassets/Volume2.imageset/Volume2.pdf differ diff --git a/BGMApp/BGMApp/Images.xcassets/Volume3.imageset/Contents.json b/BGMApp/BGMApp/Images.xcassets/Volume3.imageset/Contents.json new file mode 100644 index 0000000..8356374 --- /dev/null +++ b/BGMApp/BGMApp/Images.xcassets/Volume3.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "mac", + "filename" : "Volume3.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/BGMApp/BGMApp/Images.xcassets/Volume3.imageset/Volume3.pdf b/BGMApp/BGMApp/Images.xcassets/Volume3.imageset/Volume3.pdf new file mode 100644 index 0000000..5205d03 Binary files /dev/null and b/BGMApp/BGMApp/Images.xcassets/Volume3.imageset/Volume3.pdf differ diff --git a/BGMApp/BGMApp/Preferences/BGMPreferencesMenu.h b/BGMApp/BGMApp/Preferences/BGMPreferencesMenu.h index 71736ac..b14e1ed 100644 --- a/BGMApp/BGMApp/Preferences/BGMPreferencesMenu.h +++ b/BGMApp/BGMApp/Preferences/BGMPreferencesMenu.h @@ -17,7 +17,7 @@ // BGMPreferencesMenu.h // BGMApp // -// Copyright © 2016, 2018 Kyle Neideck +// Copyright © 2016, 2018, 2019 Kyle Neideck // // Handles the preferences menu UI. The user's preference changes are often passed directly to the driver rather // than to other BGMApp classes. @@ -26,6 +26,7 @@ // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMMusicPlayers.h" +#import "BGMStatusBarItem.h" // System Includes #import @@ -38,6 +39,7 @@ NS_ASSUME_NONNULL_BEGIN - (id) initWithBGMMenu:(NSMenu*)inBGMMenu audioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers + statusBarItem:(BGMStatusBarItem*)inStatusBarItem aboutPanel:(NSPanel*)inAboutPanel aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView; diff --git a/BGMApp/BGMApp/Preferences/BGMPreferencesMenu.mm b/BGMApp/BGMApp/Preferences/BGMPreferencesMenu.mm index 72589b7..a72f3c6 100644 --- a/BGMApp/BGMApp/Preferences/BGMPreferencesMenu.mm +++ b/BGMApp/BGMApp/Preferences/BGMPreferencesMenu.mm @@ -17,7 +17,7 @@ // BGMPreferencesMenu.mm // BGMApp // -// Copyright © 2016, 2018 Kyle Neideck +// Copyright © 2016, 2018, 2019 Kyle Neideck // // Self Include @@ -32,12 +32,19 @@ NS_ASSUME_NONNULL_BEGIN // Interface Builder tags static NSInteger const kPreferencesMenuItemTag = 1; -static NSInteger const kAboutPanelMenuItemTag = 3; +static NSInteger const kBGMIconMenuItemTag = 2; +static NSInteger const kVolumeIconMenuItemTag = 3; +static NSInteger const kAboutPanelMenuItemTag = 4; @implementation BGMPreferencesMenu { - // Menu sections + // Menu sections/items BGMAutoPauseMusicPrefs* autoPauseMusicPrefs; - + NSMenuItem* bgmIconMenuItem; + NSMenuItem* volumeIconMenuItem; + + // The menu item you press to open BGMApp's main menu. + BGMStatusBarItem* statusBarItem; + // The About Background Music window BGMAboutPanel* aboutPanel; } @@ -45,6 +52,7 @@ static NSInteger const kAboutPanelMenuItemTag = 3; - (id) initWithBGMMenu:(NSMenu*)inBGMMenu audioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers + statusBarItem:(BGMStatusBarItem*)inStatusBarItem aboutPanel:(NSPanel*)inAboutPanel aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView { if ((self = [super init])) { @@ -55,7 +63,22 @@ static NSInteger const kAboutPanelMenuItemTag = 3; musicPlayers:inMusicPlayers]; aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView]; - + + statusBarItem = inStatusBarItem; + + // Set up the menu items under the "Status Bar Icon" heading. + bgmIconMenuItem = [prefsMenu itemWithTag:kBGMIconMenuItemTag]; + bgmIconMenuItem.state = + (statusBarItem.icon == BGMFermataStatusBarIcon) ? NSOnState : NSOffState; + [bgmIconMenuItem setTarget:self]; + [bgmIconMenuItem setAction:@selector(useBGMStatusBarIcon)]; + + volumeIconMenuItem = [prefsMenu itemWithTag:kVolumeIconMenuItemTag]; + volumeIconMenuItem.state = + (statusBarItem.icon == BGMVolumeStatusBarIcon) ? NSOnState : NSOffState; + [volumeIconMenuItem setTarget:self]; + [volumeIconMenuItem setAction:@selector(useVolumeStatusBarIcon)]; + // Set up the "About Background Music" menu item NSMenuItem* aboutMenuItem = [prefsMenu itemWithTag:kAboutPanelMenuItemTag]; [aboutMenuItem setTarget:aboutPanel]; @@ -65,6 +88,29 @@ static NSInteger const kAboutPanelMenuItemTag = 3; return self; } +- (void) useBGMStatusBarIcon { + // Change the icon. + statusBarItem.icon = BGMFermataStatusBarIcon; + + // Select/deselect the menu items. + bgmIconMenuItem.state = NSOnState; + volumeIconMenuItem.state = NSOffState; +} + +- (void) useVolumeStatusBarIcon { + // TODO: Maybe we should show a message that tells the user how to hide the built-in volume + // icon. They probably won't want two status bar items that look the same. Or we might be + // able to automatically hide the built-in icon while BGMApp is running and show it again + // when BGMApp is closed. + + // Change the icon. + statusBarItem.icon = BGMVolumeStatusBarIcon; + + // Select/deselect the menu items. + bgmIconMenuItem.state = NSOffState; + volumeIconMenuItem.state = NSOnState; +} + @end NS_ASSUME_NONNULL_END diff --git a/Images/VolumeIcons.tex b/Images/VolumeIcons.tex new file mode 100644 index 0000000..ee8a306 --- /dev/null +++ b/Images/VolumeIcons.tex @@ -0,0 +1,69 @@ +% This file is part of Background Music. +% +% Background Music is free software: you can redistribute it and/or +% modify it under the terms of the GNU General Public License as +% published by the Free Software Foundation, either version 2 of the +% License, or (at your option) any later version. +% +% Background Music is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with Background Music. If not, see . + +% +% VolumeIcons.tex +% +% Build with XeTeX: +% xelatex -jobname=Volume0 '\def\UseOption{}\input{VolumeIcons.tex}' +% xelatex -jobname=Volume1 '\def\UseOption{w1}\input{VolumeIcons.tex}' +% xelatex -jobname=Volume2 '\def\UseOption{w1,w2}\input{VolumeIcons.tex}' +% xelatex -jobname=Volume3 '\def\UseOption{w1,w2,w3}\input{VolumeIcons.tex}' +% for n in 0 1 2 3; do mv Volume$n.pdf ../BGMApp/BGMApp/Images.xcassets/Volume$n.imageset/; done +% +% Might build correctly with regular LaTeX. I haven't tried it. +% + +\documentclass[tikz]{standalone} +\usepackage{tikz} +% "dummyOption" prevents "Package optional Warning: No options were selected, +% so all optional text will be printed" when building Volume0.pdf. +\usepackage[dummyOption]{optional} + +\begin{document} +\begin{tikzpicture} + +% Speaker (Rounded box and triangle) +\fill[rounded corners=5mm] + (0mm, 62.5mm) rectangle (25mm, 37.5mm) {}; +\draw[rounded corners=2.5mm,fill=black] + (3mm, 50mm)--(34mm, 76.5mm)--(34mm, 23.5mm)--cycle; + +% First sound wave (Curved line) +\opt{w1}{ + \draw[line width=4.3mm,line cap=round] + (44mm, 36.5mm) to[out=46,in=-46] (44mm, 63.5mm); +} + +% Second sound wave (Curved line) +\opt{w2}{ + \draw[line width=4.3mm,line cap=round] + (57.5mm, 27.5mm) to[out=46,in=-46] (57.5mm, 72.5mm); +} + +% Third sound wave (Curved line) +\opt{w3}{ + \draw[line width=4.3mm,line cap=round] + (72mm, 18.5mm) to[out=46,in=-46] (72mm, 81.5mm); +} + +% Always draw a transparent copy of the third wave so the images will all have +% the same width. +\draw[line width=4.3mm,line cap=round,opacity=0] + (72mm, 18.5mm) to[out=46,in=-46] (72mm, 81.5mm); + +\end{tikzpicture} +\end{document} + diff --git a/SharedSource/BGM_Types.h b/SharedSource/BGM_Types.h index 02abd70..5d14476 100644 --- a/SharedSource/BGM_Types.h +++ b/SharedSource/BGM_Types.h @@ -17,7 +17,7 @@ // BGM_Types.h // SharedSource // -// Copyright © 2016, 2017 Kyle Neideck +// Copyright © 2016, 2017, 2019 Kyle Neideck // #ifndef SharedSource__BGM_Types @@ -77,7 +77,7 @@ enum // AudioObjectPropertyElement docs: "Elements are numbered sequentially where 0 represents the // master element." -static const AudioObjectPropertyElement kMasterChannel = 0; +static const AudioObjectPropertyElement kMasterChannel = kAudioObjectPropertyElementMaster; #pragma BGM Plug-in Custom Properties