├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ └── build.yml ├── .gitignore ├── .swift-version ├── .swiftformat ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── MenuBarExtraAccess-CI.xcscheme ├── Demo ├── Demo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Demo.xcscheme └── Demo │ ├── Assets.xcassets │ ├── 0.circle.fill.symbolset │ │ ├── 0.circle.fill.svg │ │ └── Contents.json │ ├── 1.circle.fill.symbolset │ │ ├── 1.circle.fill.svg │ │ └── Contents.json │ ├── 2.circle.fill.symbolset │ │ ├── 2.circle.fill.svg │ │ └── Contents.json │ ├── 3.circle.fill.symbolset │ │ ├── 3.circle.fill.svg │ │ └── Contents.json │ ├── 4.circle.fill.symbolset │ │ ├── 4.circle.fill.svg │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Dock.swift │ ├── MenuBarExtraAccessDemoApp.entitlements │ ├── MenuBarExtraAccessDemoApp.swift │ └── MenuBarView.swift ├── LICENSE ├── Package.swift ├── README.md └── Sources └── MenuBarExtraAccess ├── MenuBarExtra Window Introspection.swift ├── MenuBarExtraAccess.swift ├── MenuBarExtraUtils ├── MenuBarExtraUtils.swift └── StatusItemIdentity.swift ├── NSControl Extensions.swift ├── NSEvent Extensions.swift ├── NSStatusItem Extensions.swift ├── NSWindow Extensions.swift ├── Unused └── Unused Code.swift └── Utilities.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: orchetect 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report about a reproducible problem. 3 | labels: bug 4 | body: 5 | - type: textarea 6 | id: bug-description 7 | attributes: 8 | label: Bug Description, Steps to Reproduce, Crash Logs, Screenshots, etc. 9 | description: "A clear and concise description of the bug and steps to reproduce. Include system details (OS version) and build environment particulars (Xcode version, etc.)." 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature request 4 | url: https://github.com/orchetect/MenuBarExtraAccess/discussions 5 | about: Suggest new features or improvements. 6 | - name: I need help setting up or troubleshooting 7 | url: https://github.com/orchetect/MenuBarExtraAccess/discussions 8 | about: Questions not answered in the documentation, discussions forum, or example projects. 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - '**/*.md' # .md files anywhere in the repo 8 | - '**/LICENSE' # LICENSE files anywhere in the repo 9 | - '**/.gitignore' # .gitignore files anywhere in the repo 10 | - '**/*.png' # .png image files anywhere in the repo 11 | - '**/*.pdf' # .pdf files anywhere in the repo 12 | 13 | pull_request: 14 | branches: [main] 15 | paths-ignore: 16 | - '**/*.md' # .md files anywhere in the repo 17 | - '**/LICENSE' # LICENSE files anywhere in the repo 18 | - '**/.gitignore' # .gitignore files anywhere in the repo 19 | - '**/*.png' # .png image files anywhere in the repo 20 | - '**/*.pdf' # .pdf files anywhere in the repo 21 | 22 | workflow_dispatch: 23 | 24 | schedule: 25 | - cron: '35 13 * * *' # once a day @ 2:35am UTC (7:35am PST) 26 | 27 | env: 28 | SCHEME: "MenuBarExtraAccess-CI" 29 | 30 | jobs: 31 | macOS-13: 32 | name: macOS 13 33 | runs-on: macos-13 34 | steps: 35 | - uses: actions/checkout@main 36 | - uses: maxim-lobanov/setup-xcode@v1 37 | with: 38 | xcode-version: latest-stable 39 | - name: Build 40 | run: xcodebuild build -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "generic/platform=macOS,name=Any Mac" | xcbeautify && exit ${PIPESTATUS[0]} 41 | # - name: Unit Tests 42 | # run: xcodebuild test -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "platform=macOS" | xcbeautify && exit ${PIPESTATUS[0]} 43 | 44 | macOS-14: 45 | name: macOS 14 46 | runs-on: macos-14 47 | steps: 48 | - uses: actions/checkout@main 49 | - uses: maxim-lobanov/setup-xcode@v1 50 | with: 51 | xcode-version: latest-stable 52 | - name: Build 53 | run: xcodebuild build -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "generic/platform=macOS,name=Any Mac" | xcbeautify && exit ${PIPESTATUS[0]} 54 | # - name: Unit Tests 55 | # run: xcodebuild test -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "platform=macOS" | xcbeautify && exit ${PIPESTATUS[0]} 56 | 57 | macOS: 58 | name: macOS 15 59 | runs-on: macos-15 60 | steps: 61 | - uses: actions/checkout@main 62 | - uses: maxim-lobanov/setup-xcode@v1 63 | with: 64 | xcode-version: latest-stable 65 | - name: Build 66 | run: xcodebuild build -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "generic/platform=macOS,name=Any Mac" | xcbeautify && exit ${PIPESTATUS[0]} 67 | # - name: Unit Tests 68 | # run: xcodebuild test -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "platform=macOS" | xcbeautify && exit ${PIPESTATUS[0]} 69 | 70 | macOS-swift6: 71 | name: macOS 15 (Swift 6) 72 | runs-on: macos-15 73 | steps: 74 | - uses: actions/checkout@main 75 | - uses: maxim-lobanov/setup-xcode@v1 76 | with: 77 | xcode-version: latest-stable 78 | - name: Set Package to Swift 6.0 79 | run: swift package tools-version --set "6.0" 80 | - name: Build 81 | run: xcodebuild build -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "generic/platform=macOS,name=Any Mac" | xcbeautify && exit ${PIPESTATUS[0]} 82 | # - name: Unit Tests 83 | # run: xcodebuild test -workspace ".swiftpm/xcode/package.xcworkspace" -scheme "$SCHEME" -destination "platform=macOS" | xcbeautify && exit ${PIPESTATUS[0]} 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | [Dd]ev/ 3 | 4 | # Xcode 5 | 6 | # macOS 7 | .DS_Store 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | ## SPM support in Xcode 40 | # .swiftpm - for shared CI schemes we need these checked in: 41 | # -> .swiftpm/xcode/package.xcworkspace 42 | # -> .swiftpm/xcode/xcshareddata/xcschemes/*.* 43 | 44 | # Swift Package Manager 45 | # 46 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 47 | Packages/ 48 | Package.pins 49 | Package.resolved 50 | .build/ 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | 58 | Pods/ 59 | 60 | # Carthage 61 | # 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | # Carthage/Checkouts 64 | 65 | Carthage/Build 66 | 67 | # fastlane 68 | # 69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 70 | # screenshots whenever they are needed. 71 | # For more information about the recommended setup visit: 72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 73 | 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots/**/*.png 77 | fastlane/test_output 78 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.7 -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --acronyms ID,URL,UUID 2 | --allman false 3 | --assetliterals visual-width 4 | --beforemarks 5 | --binarygrouping 8,8 6 | --categorymark "MARK: %c" 7 | --classthreshold 0 8 | --closingparen balanced 9 | --closurevoid remove 10 | --commas inline 11 | --conflictmarkers reject 12 | --decimalgrouping ignore 13 | --elseposition same-line 14 | --emptybraces spaced 15 | --enumthreshold 0 16 | --exponentcase lowercase 17 | --exponentgrouping disabled 18 | --extensionacl on-declarations 19 | --extensionlength 0 20 | --extensionmark "MARK: - %t + %c" 21 | --fractiongrouping disabled 22 | --fragment false 23 | --funcattributes prev-line 24 | --groupedextension "MARK: %c" 25 | --guardelse auto 26 | --header "\n {file}\n MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess\n © {year} Steffan Andrews • Licensed under MIT License\n" 27 | --hexgrouping 4,8 28 | --hexliteralcase uppercase 29 | --ifdef no-indent 30 | --importgrouping alpha 31 | --indent 4 32 | --indentcase false 33 | --indentstrings true 34 | --lifecycle 35 | --lineaftermarks true 36 | --linebreaks lf 37 | --markcategories true 38 | --markextensions always 39 | --marktypes always 40 | --maxwidth 100 41 | --modifierorder 42 | --nevertrailing 43 | --nospaceoperators 44 | --nowrapoperators 45 | --octalgrouping 4,8 46 | --operatorfunc spaced 47 | --organizetypes actor,class,enum,struct 48 | --patternlet hoist 49 | --ranges spaced 50 | --redundanttype infer-locals-only 51 | --self remove 52 | --selfrequired 53 | --semicolons inline 54 | --shortoptionals always 55 | --smarttabs enabled 56 | --stripunusedargs always 57 | --structthreshold 0 58 | --tabwidth unspecified 59 | --trailingclosures 60 | --trimwhitespace nonblank-lines 61 | --typeattributes preserve 62 | --typemark "MARK: - %t" 63 | --varattributes preserve 64 | --voidtype void 65 | --wraparguments before-first 66 | --wrapcollections before-first 67 | --wrapconditions after-first 68 | --wrapparameters before-first 69 | --wrapreturntype preserve 70 | --wrapternary before-operators 71 | --wraptypealiases before-first 72 | --xcodeindentation enabled 73 | --yodaswap always 74 | --disable blankLinesAroundMark,consecutiveSpaces,preferKeyPath,redundantParens,sortDeclarations,sortedImports,unusedArguments 75 | --enable blankLinesBetweenImports,blockComments,isEmpty,wrapEnumCases 76 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/MenuBarExtraAccess-CI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 60; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E212FC1B295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E212FC1A295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift */; }; 11 | E212FC1F295167E5007E27BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E212FC1E295167E5007E27BB /* Assets.xcassets */; }; 12 | E214EC0D2CAFA71200B91076 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E214EC0C2CAFA71200B91076 /* ContentView.swift */; }; 13 | E2B6A8A42CFBBFDA00A4B22E /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B6A8A32CFBBFD600A4B22E /* Dock.swift */; }; 14 | E2C1790B2CF45CCD00BE21A1 /* MenuBarExtraAccess in Frameworks */ = {isa = PBXBuildFile; productRef = E2C1790A2CF45CCD00BE21A1 /* MenuBarExtraAccess */; }; 15 | E2ED15CD2CAFA6B100972C1A /* MenuBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2ED15CC2CAFA6B100972C1A /* MenuBarView.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | E212FC17295167E4007E27BB /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | E212FC1A295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarExtraAccessDemoApp.swift; sourceTree = ""; }; 21 | E212FC1E295167E5007E27BB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 22 | E212FC23295167E5007E27BB /* MenuBarExtraAccessDemoApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MenuBarExtraAccessDemoApp.entitlements; sourceTree = ""; }; 23 | E214EC0C2CAFA71200B91076 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 24 | E2B6A8A32CFBBFD600A4B22E /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = ""; }; 25 | E2ED15CC2CAFA6B100972C1A /* MenuBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarView.swift; sourceTree = ""; }; 26 | /* End PBXFileReference section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | E212FC14295167E4007E27BB /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | E2C1790B2CF45CCD00BE21A1 /* MenuBarExtraAccess in Frameworks */, 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | E212FC0E295167E4007E27BB = { 41 | isa = PBXGroup; 42 | children = ( 43 | E212FC19295167E4007E27BB /* Demo */, 44 | E212FC18295167E4007E27BB /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | E212FC18295167E4007E27BB /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | E212FC17295167E4007E27BB /* Demo.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | E212FC19295167E4007E27BB /* Demo */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | E212FC1A295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift */, 60 | E2ED15CC2CAFA6B100972C1A /* MenuBarView.swift */, 61 | E214EC0C2CAFA71200B91076 /* ContentView.swift */, 62 | E2B6A8A32CFBBFD600A4B22E /* Dock.swift */, 63 | E212FC1E295167E5007E27BB /* Assets.xcassets */, 64 | E212FC23295167E5007E27BB /* MenuBarExtraAccessDemoApp.entitlements */, 65 | ); 66 | path = Demo; 67 | sourceTree = ""; 68 | }; 69 | /* End PBXGroup section */ 70 | 71 | /* Begin PBXNativeTarget section */ 72 | E212FC16295167E4007E27BB /* Demo */ = { 73 | isa = PBXNativeTarget; 74 | buildConfigurationList = E212FC26295167E5007E27BB /* Build configuration list for PBXNativeTarget "Demo" */; 75 | buildPhases = ( 76 | E212FC13295167E4007E27BB /* Sources */, 77 | E212FC14295167E4007E27BB /* Frameworks */, 78 | E212FC15295167E4007E27BB /* Resources */, 79 | ); 80 | buildRules = ( 81 | ); 82 | dependencies = ( 83 | ); 84 | name = Demo; 85 | packageProductDependencies = ( 86 | E2C1790A2CF45CCD00BE21A1 /* MenuBarExtraAccess */, 87 | ); 88 | productName = "MacControlCenterSlider Demo"; 89 | productReference = E212FC17295167E4007E27BB /* Demo.app */; 90 | productType = "com.apple.product-type.application"; 91 | }; 92 | /* End PBXNativeTarget section */ 93 | 94 | /* Begin PBXProject section */ 95 | E212FC0F295167E4007E27BB /* Project object */ = { 96 | isa = PBXProject; 97 | attributes = { 98 | BuildIndependentTargetsInParallel = 1; 99 | LastSwiftUpdateCheck = 1420; 100 | LastUpgradeCheck = 1420; 101 | TargetAttributes = { 102 | E212FC16295167E4007E27BB = { 103 | CreatedOnToolsVersion = 14.2; 104 | }; 105 | }; 106 | }; 107 | buildConfigurationList = E212FC12295167E4007E27BB /* Build configuration list for PBXProject "Demo" */; 108 | compatibilityVersion = "Xcode 13.0"; 109 | developmentRegion = en; 110 | hasScannedForEncodings = 0; 111 | knownRegions = ( 112 | en, 113 | Base, 114 | ); 115 | mainGroup = E212FC0E295167E4007E27BB; 116 | packageReferences = ( 117 | E2C179092CF45CCD00BE21A1 /* XCLocalSwiftPackageReference "../../MenuBarExtraAccess" */, 118 | ); 119 | productRefGroup = E212FC18295167E4007E27BB /* Products */; 120 | projectDirPath = ""; 121 | projectRoot = ""; 122 | targets = ( 123 | E212FC16295167E4007E27BB /* Demo */, 124 | ); 125 | }; 126 | /* End PBXProject section */ 127 | 128 | /* Begin PBXResourcesBuildPhase section */ 129 | E212FC15295167E4007E27BB /* Resources */ = { 130 | isa = PBXResourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | E212FC1F295167E5007E27BB /* Assets.xcassets in Resources */, 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXResourcesBuildPhase section */ 138 | 139 | /* Begin PBXSourcesBuildPhase section */ 140 | E212FC13295167E4007E27BB /* Sources */ = { 141 | isa = PBXSourcesBuildPhase; 142 | buildActionMask = 2147483647; 143 | files = ( 144 | E212FC1B295167E4007E27BB /* MenuBarExtraAccessDemoApp.swift in Sources */, 145 | E2ED15CD2CAFA6B100972C1A /* MenuBarView.swift in Sources */, 146 | E2B6A8A42CFBBFDA00A4B22E /* Dock.swift in Sources */, 147 | E214EC0D2CAFA71200B91076 /* ContentView.swift in Sources */, 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXSourcesBuildPhase section */ 152 | 153 | /* Begin XCBuildConfiguration section */ 154 | E212FC24295167E5007E27BB /* Debug */ = { 155 | isa = XCBuildConfiguration; 156 | buildSettings = { 157 | ALWAYS_SEARCH_USER_PATHS = NO; 158 | CLANG_ANALYZER_NONNULL = YES; 159 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 160 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 161 | CLANG_ENABLE_MODULES = YES; 162 | CLANG_ENABLE_OBJC_ARC = YES; 163 | CLANG_ENABLE_OBJC_WEAK = YES; 164 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 165 | CLANG_WARN_BOOL_CONVERSION = YES; 166 | CLANG_WARN_COMMA = YES; 167 | CLANG_WARN_CONSTANT_CONVERSION = YES; 168 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 169 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 170 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 171 | CLANG_WARN_EMPTY_BODY = YES; 172 | CLANG_WARN_ENUM_CONVERSION = YES; 173 | CLANG_WARN_INFINITE_RECURSION = YES; 174 | CLANG_WARN_INT_CONVERSION = YES; 175 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 176 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 177 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 178 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 179 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 180 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 181 | CLANG_WARN_STRICT_PROTOTYPES = YES; 182 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 183 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 184 | CLANG_WARN_UNREACHABLE_CODE = YES; 185 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 186 | COPY_PHASE_STRIP = NO; 187 | DEBUG_INFORMATION_FORMAT = dwarf; 188 | ENABLE_STRICT_OBJC_MSGSEND = YES; 189 | ENABLE_TESTABILITY = YES; 190 | GCC_C_LANGUAGE_STANDARD = gnu11; 191 | GCC_DYNAMIC_NO_PIC = NO; 192 | GCC_NO_COMMON_BLOCKS = YES; 193 | GCC_OPTIMIZATION_LEVEL = 0; 194 | GCC_PREPROCESSOR_DEFINITIONS = ( 195 | "DEBUG=1", 196 | "$(inherited)", 197 | ); 198 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 199 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 200 | GCC_WARN_UNDECLARED_SELECTOR = YES; 201 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 202 | GCC_WARN_UNUSED_FUNCTION = YES; 203 | GCC_WARN_UNUSED_VARIABLE = YES; 204 | MACOSX_DEPLOYMENT_TARGET = 14.0; 205 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 206 | MTL_FAST_MATH = YES; 207 | ONLY_ACTIVE_ARCH = YES; 208 | SDKROOT = macosx; 209 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 210 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 211 | SWIFT_VERSION = 6.0; 212 | }; 213 | name = Debug; 214 | }; 215 | E212FC25295167E5007E27BB /* Release */ = { 216 | isa = XCBuildConfiguration; 217 | buildSettings = { 218 | ALWAYS_SEARCH_USER_PATHS = NO; 219 | CLANG_ANALYZER_NONNULL = YES; 220 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 221 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 222 | CLANG_ENABLE_MODULES = YES; 223 | CLANG_ENABLE_OBJC_ARC = YES; 224 | CLANG_ENABLE_OBJC_WEAK = YES; 225 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 226 | CLANG_WARN_BOOL_CONVERSION = YES; 227 | CLANG_WARN_COMMA = YES; 228 | CLANG_WARN_CONSTANT_CONVERSION = YES; 229 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 230 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 231 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 232 | CLANG_WARN_EMPTY_BODY = YES; 233 | CLANG_WARN_ENUM_CONVERSION = YES; 234 | CLANG_WARN_INFINITE_RECURSION = YES; 235 | CLANG_WARN_INT_CONVERSION = YES; 236 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 237 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 238 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 239 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 240 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 241 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 242 | CLANG_WARN_STRICT_PROTOTYPES = YES; 243 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 244 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 245 | CLANG_WARN_UNREACHABLE_CODE = YES; 246 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 247 | COPY_PHASE_STRIP = NO; 248 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 249 | ENABLE_NS_ASSERTIONS = NO; 250 | ENABLE_STRICT_OBJC_MSGSEND = YES; 251 | GCC_C_LANGUAGE_STANDARD = gnu11; 252 | GCC_NO_COMMON_BLOCKS = YES; 253 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 255 | GCC_WARN_UNDECLARED_SELECTOR = YES; 256 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 257 | GCC_WARN_UNUSED_FUNCTION = YES; 258 | GCC_WARN_UNUSED_VARIABLE = YES; 259 | MACOSX_DEPLOYMENT_TARGET = 14.0; 260 | MTL_ENABLE_DEBUG_INFO = NO; 261 | MTL_FAST_MATH = YES; 262 | SDKROOT = macosx; 263 | SWIFT_COMPILATION_MODE = wholemodule; 264 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 265 | SWIFT_VERSION = 6.0; 266 | }; 267 | name = Release; 268 | }; 269 | E212FC27295167E5007E27BB /* Debug */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 273 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 274 | CODE_SIGN_ENTITLEMENTS = Demo/MenuBarExtraAccessDemoApp.entitlements; 275 | CODE_SIGN_STYLE = Automatic; 276 | COMBINE_HIDPI_IMAGES = YES; 277 | CURRENT_PROJECT_VERSION = 1; 278 | DEVELOPMENT_TEAM = ""; 279 | ENABLE_HARDENED_RUNTIME = YES; 280 | GENERATE_INFOPLIST_FILE = YES; 281 | INFOPLIST_KEY_CFBundleDisplayName = "MenuBarExtraAccess Demo"; 282 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 283 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 284 | LD_RUNPATH_SEARCH_PATHS = ( 285 | "$(inherited)", 286 | "@executable_path/../Frameworks", 287 | ); 288 | MARKETING_VERSION = 1.0; 289 | PRODUCT_BUNDLE_IDENTIFIER = "com.orchetect.MenuBarExtraAccess.Demo${DEVELOPMENT_TEAM}"; 290 | PRODUCT_NAME = "$(TARGET_NAME)"; 291 | SUPPORTED_PLATFORMS = macosx; 292 | SUPPORTS_MACCATALYST = NO; 293 | SWIFT_EMIT_LOC_STRINGS = YES; 294 | }; 295 | name = Debug; 296 | }; 297 | E212FC28295167E5007E27BB /* Release */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 301 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 302 | CODE_SIGN_ENTITLEMENTS = Demo/MenuBarExtraAccessDemoApp.entitlements; 303 | CODE_SIGN_STYLE = Automatic; 304 | COMBINE_HIDPI_IMAGES = YES; 305 | CURRENT_PROJECT_VERSION = 1; 306 | DEVELOPMENT_TEAM = ""; 307 | ENABLE_HARDENED_RUNTIME = YES; 308 | GENERATE_INFOPLIST_FILE = YES; 309 | INFOPLIST_KEY_CFBundleDisplayName = "MenuBarExtraAccess Demo"; 310 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 311 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 312 | LD_RUNPATH_SEARCH_PATHS = ( 313 | "$(inherited)", 314 | "@executable_path/../Frameworks", 315 | ); 316 | MARKETING_VERSION = 1.0; 317 | PRODUCT_BUNDLE_IDENTIFIER = "com.orchetect.MenuBarExtraAccess.Demo${DEVELOPMENT_TEAM}"; 318 | PRODUCT_NAME = "$(TARGET_NAME)"; 319 | SUPPORTED_PLATFORMS = macosx; 320 | SUPPORTS_MACCATALYST = NO; 321 | SWIFT_EMIT_LOC_STRINGS = YES; 322 | }; 323 | name = Release; 324 | }; 325 | /* End XCBuildConfiguration section */ 326 | 327 | /* Begin XCConfigurationList section */ 328 | E212FC12295167E4007E27BB /* Build configuration list for PBXProject "Demo" */ = { 329 | isa = XCConfigurationList; 330 | buildConfigurations = ( 331 | E212FC24295167E5007E27BB /* Debug */, 332 | E212FC25295167E5007E27BB /* Release */, 333 | ); 334 | defaultConfigurationIsVisible = 0; 335 | defaultConfigurationName = Release; 336 | }; 337 | E212FC26295167E5007E27BB /* Build configuration list for PBXNativeTarget "Demo" */ = { 338 | isa = XCConfigurationList; 339 | buildConfigurations = ( 340 | E212FC27295167E5007E27BB /* Debug */, 341 | E212FC28295167E5007E27BB /* Release */, 342 | ); 343 | defaultConfigurationIsVisible = 0; 344 | defaultConfigurationName = Release; 345 | }; 346 | /* End XCConfigurationList section */ 347 | 348 | /* Begin XCLocalSwiftPackageReference section */ 349 | E2C179092CF45CCD00BE21A1 /* XCLocalSwiftPackageReference "../../MenuBarExtraAccess" */ = { 350 | isa = XCLocalSwiftPackageReference; 351 | relativePath = ../../MenuBarExtraAccess; 352 | }; 353 | /* End XCLocalSwiftPackageReference section */ 354 | 355 | /* Begin XCSwiftPackageProductDependency section */ 356 | E2C1790A2CF45CCD00BE21A1 /* MenuBarExtraAccess */ = { 357 | isa = XCSwiftPackageProductDependency; 358 | productName = MenuBarExtraAccess; 359 | }; 360 | /* End XCSwiftPackageProductDependency section */ 361 | }; 362 | rootObject = E212FC0F295167E4007E27BB /* Project object */; 363 | } 364 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/0.circle.fill.symbolset/0.circle.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | Weight/Scale Variations 12 | Ultralight 13 | Thin 14 | Light 15 | Regular 16 | Medium 17 | Semibold 18 | Bold 19 | Heavy 20 | Black 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Design Variations 32 | Symbols are supported in up to nine weights and three scales. 33 | For optimal layout with text and other symbols, vertically align 34 | symbols with the adjacent text. 35 | 36 | 37 | 38 | 39 | 40 | Margins 41 | Leading and trailing margins on the left and right side of each symbol 42 | can be adjusted by modifying the x-location of the margin guidelines. 43 | Modifications are automatically applied proportionally to all 44 | scales and weights. 45 | 46 | 47 | 48 | Exporting 49 | Symbols should be outlined when exporting to ensure the 50 | design is preserved when submitting to Xcode. 51 | Template v.2.0 52 | Requires Xcode 12 or greater 53 | Generated from 0.circle.fill 54 | Typeset at 100 points 55 | Small 56 | Medium 57 | Large 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/0.circle.fill.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "0.circle.fill.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/1.circle.fill.symbolset/1.circle.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | Weight/Scale Variations 12 | Ultralight 13 | Thin 14 | Light 15 | Regular 16 | Medium 17 | Semibold 18 | Bold 19 | Heavy 20 | Black 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Design Variations 32 | Symbols are supported in up to nine weights and three scales. 33 | For optimal layout with text and other symbols, vertically align 34 | symbols with the adjacent text. 35 | 36 | 37 | 38 | 39 | 40 | Margins 41 | Leading and trailing margins on the left and right side of each symbol 42 | can be adjusted by modifying the x-location of the margin guidelines. 43 | Modifications are automatically applied proportionally to all 44 | scales and weights. 45 | 46 | 47 | 48 | Exporting 49 | Symbols should be outlined when exporting to ensure the 50 | design is preserved when submitting to Xcode. 51 | Template v.2.0 52 | Requires Xcode 12 or greater 53 | Generated from 1.circle.fill 54 | Typeset at 100 points 55 | Small 56 | Medium 57 | Large 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/1.circle.fill.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "1.circle.fill.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/2.circle.fill.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "2.circle.fill.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/3.circle.fill.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "3.circle.fill.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/4.circle.fill.symbolset/4.circle.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | Weight/Scale Variations 12 | Ultralight 13 | Thin 14 | Light 15 | Regular 16 | Medium 17 | Semibold 18 | Bold 19 | Heavy 20 | Black 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Design Variations 32 | Symbols are supported in up to nine weights and three scales. 33 | For optimal layout with text and other symbols, vertically align 34 | symbols with the adjacent text. 35 | 36 | 37 | 38 | 39 | 40 | Margins 41 | Leading and trailing margins on the left and right side of each symbol 42 | can be adjusted by modifying the x-location of the margin guidelines. 43 | Modifications are automatically applied proportionally to all 44 | scales and weights. 45 | 46 | 47 | 48 | Exporting 49 | Symbols should be outlined when exporting to ensure the 50 | design is preserved when submitting to Xcode. 51 | Template v.2.0 52 | Requires Xcode 12 or greater 53 | Generated from 4.circle.fill 54 | Typeset at 100 points 55 | Small 56 | Medium 57 | Large 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/4.circle.fill.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "4.circle.fill.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuBarExtraAccessDemoApp.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftUI 8 | 9 | struct ContentView: View { 10 | @Binding var isMenu0Presented: Bool 11 | @Binding var isMenu1Presented: Bool 12 | @Binding var isMenu2Presented: Bool 13 | @Binding var isMenu3Presented: Bool 14 | @Binding var isMenu4Presented: Bool 15 | 16 | @State private var dock = Dock() 17 | 18 | var body: some View { 19 | VStack(spacing: 40) { 20 | HStack(spacing: 20) { 21 | MenuStateView(num: 4, isMenuPresented: $isMenu4Presented) 22 | MenuStateView(num: 3, isMenuPresented: $isMenu3Presented) 23 | MenuStateView(num: 2, isMenuPresented: $isMenu2Presented) 24 | MenuStateView(num: 1, isMenuPresented: $isMenu1Presented) 25 | MenuStateView(num: 0, isMenuPresented: $isMenu0Presented) 26 | } 27 | 28 | Toggle("Dock Icon Visible", isOn: $dock.isVisible) 29 | 30 | Text( 31 | """ 32 | Try opening and closing the menus in the status bar. The toggles will update in response because the state binding is being updated. Clicking on the toggles will also open or close the menus by setting the binding value.") 33 | 34 | Note that due to how Apple implemented MenuBarExtra, menu-based status items hijack the main runloop and therefore you won't see state update here if you click on their status bar button. Additionally, they cannot be dismissed by setting the binding to false because no SwiftUI state updates occur while the menu is open - the user must select a menu item or dismiss the menu. But the menu can be opened programmatically by setting the binding to true. 35 | """ 36 | ) 37 | } 38 | .padding() 39 | .frame(minWidth: 400, minHeight: 350) 40 | .toggleStyle(.switch) 41 | } 42 | 43 | struct MenuStateView: View { 44 | let num: Int 45 | @Binding var isMenuPresented: Bool 46 | 47 | var body: some View { 48 | VStack(spacing: 20) { 49 | Image(systemName: "\(num).circle.fill") 50 | .resizable() 51 | .frame(width: 20, height: 20) 52 | Toggle("", isOn: $isMenuPresented) 53 | .toggleStyle(.switch) 54 | .labelsHidden() 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/Demo/Dock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dock.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | import SwiftUI 9 | 10 | @Observable @MainActor final class Dock { 11 | var isVisible: Bool { 12 | get { NSApplication.shared.activationPolicy() == .regular } 13 | set { NSApplication.shared.setActivationPolicy(newValue ? .regular : .accessory) } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Demo/Demo/MenuBarExtraAccessDemoApp.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo/Demo/MenuBarExtraAccessDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuBarExtraAccessDemoApp.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftUI 8 | import MenuBarExtraAccess 9 | 10 | @main 11 | struct MenuBarExtraAccessDemoApp: App { 12 | @State var isMenu0Presented: Bool = false 13 | @State var isMenu1Presented: Bool = false 14 | @State var isMenu2Presented: Bool = false 15 | @State var isMenu3Presented: Bool = false 16 | @State var isMenu4Presented: Bool = false 17 | 18 | @State var menu0StatusItem: NSStatusItem? 19 | 20 | var body: some Scene { 21 | // MARK: - Info Window 22 | 23 | WindowGroup { 24 | ContentView( 25 | isMenu0Presented: $isMenu0Presented, 26 | isMenu1Presented: $isMenu1Presented, 27 | isMenu2Presented: $isMenu2Presented, 28 | isMenu3Presented: $isMenu3Presented, 29 | isMenu4Presented: $isMenu4Presented 30 | ) 31 | } 32 | .windowResizability(.contentSize) 33 | 34 | // MARK: - MenuBarExtra Scenes 35 | 36 | // 💡 NOTE: There are 5 menu extras here simply to demonstrate (and test) 37 | // various implementations of MenuBarExtra 38 | 39 | // 💡 NOTE: Even if the menu extras get reordered in the menu bar by the user (by holding Cmd 40 | // and dragging them), the indexes still remain consistent with the order in which 41 | // the MenuBarExtra definitions appear below. 42 | 43 | // MARK: Standard Menu 44 | 45 | MenuBarExtra("Menu: Index 0", systemImage: "0.circle.fill") { 46 | Button("Menu Item A") { print("Menu Item A") } 47 | Button("Menu Item B") { print("Menu Item B") } 48 | } 49 | .menuBarExtraAccess(index: 0, isPresented: $isMenu0Presented) { statusItem in 50 | // can do one-time setup of NSStatusItem here or if access to it 51 | // is needed later, it may be stored in a local state var like this: 52 | menu0StatusItem = statusItem 53 | } 54 | .menuBarExtraStyle(.menu) 55 | 56 | // MARK: Standard menu using named image 57 | 58 | MenuBarExtra("Menu: Index 1", image: "1.circle.fill") { 59 | Button("Menu Item A") { print("Menu Item A") } 60 | Button("Menu Item B") { print("Menu Item B") } 61 | Button("Menu Item C") { print("Menu Item C") } 62 | Button("Toggle Menu 0 Disabled State") { 63 | menu0StatusItem?.button?.appearsDisabled.toggle() 64 | } 65 | } 66 | .menuBarExtraAccess(index: 1, isPresented: $isMenu1Presented) 67 | .menuBarExtraStyle(.menu) 68 | 69 | // MARK: Window-style using systemImage 70 | 71 | MenuBarExtra("Menu: Index 2", systemImage: "2.circle.fill") { 72 | MenuBarView(index: 2, isMenuPresented: $isMenu2Presented) 73 | } 74 | .menuBarExtraAccess(index: 2, isPresented: $isMenu2Presented) 75 | .menuBarExtraStyle(.window) 76 | 77 | // MARK: Window-style using named image 78 | 79 | MenuBarExtra("Menu: Index 3", image: "3.circle.fill") { 80 | MenuBarView(index: 3, isMenuPresented: $isMenu3Presented) 81 | .introspectMenuBarExtraWindow(index: 3) { window in 82 | window.alphaValue = 0.5 83 | } 84 | } 85 | .menuBarExtraAccess(index: 3, isPresented: $isMenu3Presented) 86 | .menuBarExtraStyle(.window) 87 | 88 | // MARK: Window-style using custom label 89 | 90 | MenuBarExtra { 91 | MenuBarView(index: 4, isMenuPresented: $isMenu4Presented) 92 | } label: { 93 | Image(systemName: "4.circle.fill") 94 | Text("Four") 95 | } 96 | .menuBarExtraAccess(index: 4, isPresented: $isMenu4Presented) 97 | .menuBarExtraStyle(.window) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Demo/Demo/MenuBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuBarExtraAccessDemoApp.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import SwiftUI 8 | 9 | struct MenuBarView: View { 10 | let index: Int 11 | @Binding var isMenuPresented: Bool 12 | 13 | var body: some View { 14 | VStack(spacing: 40) { 15 | Image(systemName: "\(index).circle.fill") 16 | .resizable() 17 | .foregroundColor(.secondary) 18 | .frame(width: 80, height: 80) 19 | Button("Close Menu") { 20 | isMenuPresented = false 21 | } 22 | } 23 | .padding() 24 | .frame(width: 250, height: 300) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Steffan Andrews - https://github.com/orchetect 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "MenuBarExtraAccess", 7 | platforms: [.macOS(.v10_15)], 8 | products: [ 9 | .library(name: "MenuBarExtraAccess", targets: ["MenuBarExtraAccess"]) 10 | ], 11 | targets: [ 12 | .target( 13 | name: "MenuBarExtraAccess", 14 | swiftSettings: [ 15 | // un-comment to enable debug logging 16 | // .define("MENUBAREXTRAACCESS_DEBUG_LOGGING=1") 17 | ] 18 | ) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MenuBarExtraAccess 2 | 3 | [![Platforms - macOS 13.0](https://img.shields.io/badge/platforms-macOS%2013.0-blue.svg?style=flat)](https://developer.apple.com/swift) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Forchetect%2FMenuBarExtraAccess%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/orchetect/MenuBarExtraAccess) [![Xcode 14](https://img.shields.io/badge/Xcode-14-blue.svg?style=flat)](https://developer.apple.com/swift) [![License: MIT](http://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](https://github.com/orchetect/MenuBarExtraAccess/blob/main/LICENSE) 4 | 5 | #### **Gives you *Extra* access to SwiftUI `MenuBarExtra`.** 6 | 7 | - Programmatically hide, show, or toggle the menu (by way of a Bool binding) 8 | - Access to the underlying `NSStatusItem` 9 | - Access to the underlying `NSWindow` (when using the `.window` style) 10 | - Works with one or [multiple](#Multiple-MenuBarExtra) `MenuBarExtra` 11 | - Works with both [`menu`](#Standard-Menu-Style) and [`window`](#Window-Style) based styles (see [Known Issues](#Known-Issues)) 12 | 13 | #### Why? 14 | 15 | There is no 1st-party MenuBarExtra API to get or set the menu presentation state, access the status item, or access the popup's NSWindow. (Still as of Xcode 16.1) 16 | 17 | #### Library Features 18 | 19 | - A new `.menuBarExtraAccess(isPresented:) { statusItem in }` scene modifier with 20 | - a binding to hide/show/toggle the menu, and 21 | - direct access to the `NSStatusItem` if needed 22 | - A new `.introspectMenuBarExtraWindow { window in }` view modifier passing in the `NSWindow` reference 23 | - Window-based menu extra status items now remain highlighted while the window is open so it feels more like a native menu 24 | - No private API used, so it's Mac App Store safe 25 | 26 | ## Getting Started 27 | 28 | The library is available as a Swift Package Manager (SPM) package. 29 | 30 | Use the URL `https://github.com/orchetect/MenuBarExtraAccess` when adding the library to a project or Swift package. 31 | 32 | Then import the library: 33 | 34 | ```swift 35 | import SwiftUI 36 | import MenuBarExtraAccess 37 | ``` 38 | 39 | ### Standard Menu Style 40 | 41 | An example of showing the menu extra menu by clicking a button in a window: 42 | 43 | ```swift 44 | @main struct MyApp: App { 45 | @State var isMenuPresented: Bool = false 46 | 47 | var body: some Scene { 48 | WindowGroup { 49 | Button("Show Menu") { isMenuPresented = true } 50 | } 51 | 52 | MenuBarExtra("MyApp Menu", systemImage: "folder") { 53 | Button("Menu Item 1") { print("Menu Item 1") } 54 | Button("Menu Item 2") { print("Menu Item 2") } 55 | } 56 | .menuBarExtraStyle(.menu) 57 | .menuBarExtraAccess(isPresented: $isMenuPresented) { statusItem in // <-- the magic ✨ 58 | // access status item or store it in a @State var 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | ### Window Style 65 | 66 | An example of a button in the popup window dismissing the popup and performing an action: 67 | 68 | ```swift 69 | @main struct MyApp: App { 70 | @State var isMenuPresented: Bool = false 71 | 72 | var body: some Scene { 73 | MenuBarExtra("MyApp Menu", systemImage: "folder") { 74 | MyMenu(isMenuPresented: $isMenuPresented) 75 | .introspectMenuBarExtraWindow { window in // <-- the magic ✨ 76 | window.animationBehavior = .alertPanel 77 | } 78 | } 79 | .menuBarExtraStyle(.window) 80 | .menuBarExtraAccess(isPresented: $isMenuPresented) { statusItem in // <-- the magic ✨ 81 | // access status item or store it in a @State var 82 | } 83 | } 84 | } 85 | 86 | struct MyMenu: View { 87 | @Binding var isMenuPresented: Bool 88 | 89 | var body: some View { 90 | Button("Perform Action") { 91 | isMenuPresented = false 92 | performSomeAction() 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | ### Multiple MenuBarExtra 99 | 100 | MenuBarExtraAccess is fully compatible with one or multiple MenuBarExtra in an app. 101 | 102 | Just add an index number parameter to `.menuBarExtraAccess()` and `.introspectMenuBarExtraWindow()` that reflects the order of `MenuBarExtra` declarations. 103 | 104 | ```swift 105 | var body: some Scene { 106 | MenuBarExtra("MyApp Menu A", systemImage: "folder") { 107 | MyMenu(isMenuPresented: $isMenuPresented) 108 | .introspectMenuBarExtraWindow(index: 0) { window in // <-- add index 0 109 | // ... 110 | } 111 | } 112 | .menuBarExtraStyle(.window) 113 | .menuBarExtraAccess(index: 0, isPresented: $isMenuPresented) // <-- add index 0 114 | 115 | MenuBarExtra("MyApp Menu B", systemImage: "folder") { 116 | MyMenu(isMenuPresented: $isMenuPresented) 117 | .introspectMenuBarExtraWindow(index: 1) { window in // <-- add index 1 118 | // ... 119 | } 120 | } 121 | .menuBarExtraStyle(.window) 122 | .menuBarExtraAccess(index: 1, isPresented: $isMenuPresented) // <-- add index 1 123 | } 124 | ``` 125 | 126 | ## Future 127 | 128 | The hope is that Apple implements native versions of these features (and more) in future iterations of SwiftUI! 129 | 130 | Until then, a radar has been filed as a feature request: [FB11984872](https://github.com/feedback-assistant/reports/issues/383) 131 | 132 | ## Menu Builder 133 | 134 | Check out [MacControlCenterUI](https://github.com/orchetect/MacControlCenterUI), a SwiftUI package built on MenuBarExtraAccess for easily building Control Center style menus. 135 | 136 | ## Known Issues 137 | 138 | - When using `.menuBarExtraStyle(.menu)`, SwiftUI causes the popup menu to block the runloop while the menu is open, which means: 139 | - Observing the `isPresented` binding will not work as expected. 140 | - Setting the `isPresented` binding to `false` while the menu is presented has no effect. 141 | - The user must dismiss the menu themself to allow event flow to continue. We have no control over this until Apple decides to change the MenuBarExtra behavior. 142 | 143 | ## Author 144 | 145 | Coded by a bunch of 🐹 hamsters in a trenchcoat that calls itself [@orchetect](https://github.com/orchetect). 146 | 147 | ## License 148 | 149 | Licensed under the MIT license. See [LICENSE](https://github.com/orchetect/MenuBarExtraAccess/blob/master/LICENSE) for details. 150 | 151 | ## Sponsoring 152 | 153 | If you enjoy using MenuBarExtraAccess and want to contribute to open-source financially, GitHub sponsorship is much appreciated. Feedback and code contributions are also welcome. 154 | 155 | ## Contributions 156 | 157 | Contributions are welcome. Posting in [Discussions](https://github.com/orchetect/MenuBarExtraAccess/discussions) first prior to new submitting PRs for features or modifications is encouraged. 158 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/MenuBarExtra Window Introspection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuBarExtra Window Introspection.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(macOS) 8 | 9 | import SwiftUI 10 | 11 | @MainActor // required for Xcode 15 builds 12 | extension View { 13 | /// Provides introspection on the underlying window presented by `MenuBarExtra`. 14 | /// Add this view modifier to the top level of the View that occupies the `MenuBarExtra` content. 15 | /// If more than one MenuBarExtra are used in the app, provide the sequential index number of the `MenuBarExtra`. 16 | public func introspectMenuBarExtraWindow( 17 | index: Int = 0, 18 | _ block: @escaping (_ window: NSWindow) -> Void 19 | ) -> some View { 20 | self 21 | .onAppear { 22 | guard let window = MenuBarExtraUtils.window(for: .index(index)) else { 23 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 24 | print("Cannot call introspection block for status item because its window could not be found.") 25 | #endif 26 | 27 | return 28 | } 29 | 30 | block(window) 31 | } 32 | } 33 | } 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/MenuBarExtraAccess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuBarExtraAccess.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(macOS) 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | @available(macOS 13.0, *) 13 | @available(iOS, unavailable) 14 | @available(tvOS, unavailable) 15 | @available(watchOS, unavailable) 16 | @MainActor // required for Xcode 15 builds 17 | extension Scene { 18 | /// Adds a presentation state binding to `MenuBarExtra`. 19 | /// If more than one MenuBarExtra are used in the app, provide the sequential index number of the `MenuBarExtra`. 20 | public func menuBarExtraAccess( 21 | index: Int = 0, 22 | isPresented: Binding, 23 | statusItem: (@MainActor @Sendable (_ statusItem: NSStatusItem) -> Void)? = nil 24 | ) -> some Scene { 25 | // FYI: SwiftUI will reinitialize the MenuBarExtra (and this view modifier) 26 | // if its title/label content changes, which means the stored ID will always be up-to-date 27 | 28 | MenuBarExtraAccess( 29 | index: index, 30 | statusItemIntrospection: statusItem, 31 | menuBarExtra: self, 32 | isMenuPresented: isPresented 33 | ) 34 | } 35 | } 36 | 37 | @available(macOS 13.0, *) 38 | @available(iOS, unavailable) 39 | @available(tvOS, unavailable) 40 | @available(watchOS, unavailable) 41 | @MainActor // required for Xcode 15 builds 42 | struct MenuBarExtraAccess: Scene { 43 | let index: Int 44 | let statusItemIntrospection: (@MainActor @Sendable (_ statusItem: NSStatusItem) -> Void)? 45 | let menuBarExtra: Content 46 | @Binding var isMenuPresented: Bool 47 | 48 | init( 49 | index: Int, 50 | statusItemIntrospection: (@MainActor @Sendable (_ statusItem: NSStatusItem) -> Void)?, 51 | menuBarExtra: Content, 52 | isMenuPresented: Binding 53 | ) { 54 | self.index = index 55 | self.statusItemIntrospection = statusItemIntrospection 56 | self.menuBarExtra = menuBarExtra 57 | self._isMenuPresented = isMenuPresented 58 | } 59 | 60 | var body: some Scene { 61 | menuBarExtra 62 | .onChange(of: observerSetup()) { newValue in 63 | // do nothing here - the method runs setup when polled by SwiftUI 64 | } 65 | .onChange(of: isMenuPresented) { newValue in 66 | setPresented(newValue) 67 | } 68 | } 69 | 70 | private func togglePresented() { 71 | MenuBarExtraUtils.togglePresented(for: .index(index)) 72 | } 73 | 74 | private func setPresented(_ state: Bool) { 75 | MenuBarExtraUtils.setPresented(for: .index(index), state: state) 76 | } 77 | 78 | // MARK: Observer 79 | 80 | /// A workaround since `onAppear {}` is not available in a SwiftUI Scene. 81 | /// We need to set up the observer, but it can't be set up in the scene init because it needs to 82 | /// update scene state from an escaping closure. 83 | /// This returns a bogus value, but because we call it in an `onChange {}` block, SwiftUI 84 | /// is forced to evaluate the method and run our code at the appropriate time. 85 | private func observerSetup() -> Int { 86 | observerContainer.setupStatusItemIntrospection { 87 | guard let statusItem = MenuBarExtraUtils.statusItem(for: .index(index)) else { return } 88 | statusItemIntrospection?(statusItem) 89 | } 90 | 91 | // note that we can't use the button state value itself since MenuBarExtra seems to treat it 92 | // as a toggle and not an absolute on/off value. Its polarity can invert itself when clicking 93 | // in an empty area of the menubar or a different app's status item in order to dismiss the window, 94 | // for example. 95 | observerContainer.setupStatusItemButtonStateObserver { 96 | MenuBarExtraUtils.newStatusItemButtonStateObserver(index: index) { change in 97 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 98 | print("Status item button state observer: called with change: \(change.newValue?.description ?? "nil")") 99 | #endif 100 | 101 | // only continue if the MenuBarExtra is menu-based. 102 | // window-based MenuBarExtras are handled with app-bound window observers instead. 103 | guard MenuBarExtraUtils.statusItem(for: .index(index))? 104 | .isMenuBarExtraMenuBased == true 105 | else { return } 106 | 107 | guard let newVal = change.newValue else { return } 108 | let newBool = newVal != .off 109 | if isMenuPresented != newBool { 110 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 111 | print("Status item button state observer: Setting isMenuPresented to \(newBool)") 112 | #endif 113 | 114 | isMenuPresented = newBool 115 | } 116 | } 117 | } 118 | 119 | // TODO: this mouse event observer is now redundant and can be deleted in the future 120 | 121 | // observerContainer.setupGlobalMouseDownMonitor { 122 | // // note that this won't fire when mouse events within the app cause the window to dismiss 123 | // MenuBarExtraUtils.newGlobalMouseDownEventsMonitor { event in 124 | // #if MENUBAREXTRAACCESS_DEBUG_LOGGING 125 | // print("Global mouse-down events monitor: called with event: \(event.type.name)") 126 | // #endif 127 | // 128 | // // close window when user clicks outside of it 129 | // 130 | // MenuBarExtraUtils.setPresented(for: .index(index), state: false) 131 | // 132 | // #if MENUBAREXTRAACCESS_DEBUG_LOGGING 133 | // print("Global mouse-down events monitor: Setting isMenuPresented to false") 134 | // #endif 135 | // 136 | // isMenuPresented = false 137 | // } 138 | // } 139 | 140 | observerContainer.setupWindowObservers( 141 | index: index, 142 | didBecomeKey: { window in 143 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 144 | print("MenuBarExtra index \(index) drop-down window did become key.") 145 | #endif 146 | 147 | MenuBarExtraUtils.setKnownPresented(for: .index(index), state: true) 148 | isMenuPresented = true 149 | }, 150 | didResignKey: { window in 151 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 152 | print("MenuBarExtra index \(index) drop-down window did resign as key.") 153 | #endif 154 | 155 | // it's possible for a window to resign key without actually closing, so let's 156 | // close it as a failsafe. 157 | if window.isVisible { 158 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 159 | print("Closing MenuBarExtra index \(index) drop-down window as a result of it resigning as key.") 160 | #endif 161 | 162 | window.close() 163 | } 164 | 165 | MenuBarExtraUtils.setKnownPresented(for: .index(index), state: false) 166 | isMenuPresented = false 167 | } 168 | ) 169 | 170 | return 0 171 | } 172 | 173 | // MARK: Observers 174 | 175 | private var observerContainer = ObserverContainer() 176 | 177 | @MainActor 178 | private class ObserverContainer { 179 | private var statusItemIntrospectionSetup: Bool = false 180 | private var observer: NSStatusItem.ButtonStateObserver? 181 | private var eventsMonitor: Any? 182 | private var windowDidBecomeKeyObserver: AnyCancellable? 183 | private var windowDidResignKeyObserver: AnyCancellable? 184 | 185 | init() { } 186 | 187 | func setupStatusItemIntrospection( 188 | _ block: @MainActor @escaping @Sendable () -> Void 189 | ) { 190 | guard !statusItemIntrospectionSetup else { return } 191 | // run async so that it can execute after SwiftUI sets up the NSStatusItem 192 | Task { @MainActor in 193 | block() 194 | } 195 | } 196 | 197 | func setupStatusItemButtonStateObserver( 198 | _ block: @MainActor @escaping @Sendable () -> NSStatusItem.ButtonStateObserver? 199 | ) { 200 | // run async so that it can execute after SwiftUI sets up the NSStatusItem 201 | Task { @MainActor [self] in 202 | observer = block() 203 | } 204 | } 205 | 206 | func setupGlobalMouseDownMonitor( 207 | _ block: @MainActor @escaping @Sendable () -> Any? 208 | ) { 209 | // run async so that it can execute after SwiftUI sets up the NSStatusItem 210 | Task { @MainActor [self] in 211 | // tear down old monitor, if one exists 212 | if let eventsMonitor = eventsMonitor { 213 | NSEvent.removeMonitor(eventsMonitor) 214 | } 215 | 216 | eventsMonitor = block() 217 | } 218 | } 219 | 220 | func setupWindowObservers( 221 | index: Int, 222 | didBecomeKey didBecomeKeyBlock: @MainActor @escaping @Sendable (_ window: NSWindow) -> Void, 223 | didResignKey didResignKeyBlock: @MainActor @escaping @Sendable (_ window: NSWindow) -> Void 224 | ) { 225 | // run async so that it can execute after SwiftUI sets up the NSStatusItem 226 | Task { @MainActor [self] in 227 | windowDidBecomeKeyObserver = MenuBarExtraUtils.newWindowObserver( 228 | index: index, 229 | for: NSWindow.didBecomeKeyNotification 230 | ) { window in didBecomeKeyBlock(window) } 231 | 232 | windowDidResignKeyObserver = MenuBarExtraUtils.newWindowObserver( 233 | index: index, 234 | for: NSWindow.didResignKeyNotification 235 | ) { window in didResignKeyBlock(window) } 236 | 237 | } 238 | } 239 | } 240 | } 241 | 242 | #endif 243 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/MenuBarExtraUtils/MenuBarExtraUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuBarExtraUtils.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(macOS) 8 | 9 | import AppKit 10 | import SwiftUI 11 | import Combine 12 | 13 | /// Global static utility methods for interacting the app's menu bar extras (status items). 14 | @MainActor 15 | enum MenuBarExtraUtils { 16 | // MARK: - Menu Extra Manipulation 17 | 18 | /// Toggle MenuBarExtra menu/window presentation state. 19 | static func togglePresented(for ident: StatusItemIdentity? = nil) { 20 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 21 | print("MenuBarExtraUtils.\(#function) called for status item \(ident?.description ?? "nil")") 22 | #endif 23 | 24 | statusItem(for: ident)?.togglePresented() 25 | } 26 | 27 | /// Set MenuBarExtra menu/window presentation state. 28 | static func setPresented(for ident: StatusItemIdentity? = nil, state: Bool) { 29 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 30 | print("MenuBarExtraUtils.\(#function) called for status item \(ident?.description ?? "nil") with state \(state)") 31 | #endif 32 | 33 | guard let item = statusItem(for: ident) else { return } 34 | item.setPresented(state: state) 35 | } 36 | 37 | /// Set MenuBarExtra menu/window presentation state only when its state is reliably known. 38 | static func setKnownPresented(for ident: StatusItemIdentity? = nil, state: Bool) { 39 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 40 | print("MenuBarExtraUtils.\(#function) called for status item \(ident?.description ?? "nil") with state \(state)") 41 | #endif 42 | 43 | guard let item = statusItem(for: ident) else { return } 44 | item.setKnownPresented(state: state) 45 | } 46 | } 47 | 48 | // MARK: - Objects and Metadata 49 | 50 | @MainActor 51 | extension MenuBarExtraUtils { 52 | /// Returns the underlying status item(s) created by `MenuBarExtra` instances. 53 | /// 54 | /// Each `MenuBarExtra` creates one status item. 55 | /// 56 | /// If the `isInserted` binding on a `MenuBarExtra` is set to false, it may not return a status 57 | /// item. This may also change its index. 58 | static var statusItems: [NSStatusItem] { 59 | NSApp.windows 60 | .filter { 61 | $0.className.contains("NSStatusBarWindow") 62 | } 63 | .compactMap { window -> NSStatusItem? in 64 | // On Macs with only one display, there should only be one result. 65 | // On Macs with two or more displays and system prefs set to "Displays have Separate 66 | // Spaces", one NSStatusBarWindow instance per display will be returned. 67 | // - the main/active instance has a statusItem property of type NSStatusItem 68 | // - the other(s) have a statusItem property of type NSStatusItemReplicant 69 | 70 | // NSStatusItemReplicant is a replica for displaying the status item on inactive 71 | // spaces/screens that happens to be an NSStatusItem subclass. 72 | // both respond to the action selector being sent to them. 73 | // We only need to interact with the main non-replica status item. 74 | guard let statusItem = window.fetchStatusItem(), 75 | statusItem.className == "NSStatusItem" 76 | else { return nil } 77 | return statusItem 78 | } 79 | } 80 | 81 | /// Returns the underlying status items created by `MenuBarExtra` for the 82 | /// `MenuBarExtra` with the specified index. 83 | /// 84 | /// Each `MenuBarExtra` creates one status item. 85 | /// 86 | /// If the `isInserted` binding on a `MenuBarExtra` is set to false, it may not return a status 87 | /// item. This may also change its index. 88 | static func statusItem(for ident: StatusItemIdentity? = nil) -> NSStatusItem? { 89 | let statusItems = statusItems 90 | 91 | guard let ident else { return statusItems.first } 92 | 93 | switch ident { 94 | case .id(let menuBarExtraID): 95 | return statusItems.filter { $0.menuBarExtraID == menuBarExtraID }.first 96 | case .index(let index): 97 | guard statusItems.indices.contains(index) else { return nil } 98 | return statusItems[index] 99 | } 100 | } 101 | 102 | /// Returns window associated with a window-based MenuBarExtra. 103 | /// Always returns `nil` for a menu-based MenuBarExtra. 104 | static func window(for ident: StatusItemIdentity? = nil) -> NSWindow? { 105 | // we can't use NSStatusItem.window because it won't work 106 | 107 | let menuBarWindows = NSApp.windows.filter { 108 | $0.className.contains("MenuBarExtraWindow") 109 | } 110 | 111 | guard let ident else { return menuBarWindows.first } 112 | 113 | switch ident { 114 | case .id(let menuBarExtraID): 115 | guard let match = menuBarWindows.first(where: { $0.menuBarExtraID == menuBarExtraID }) else { 116 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 117 | print("MenuBarExtraUtils.\(#function): Window could not be found for status item with ID \"\(menuBarExtraID).") 118 | #endif 119 | 120 | return nil 121 | } 122 | return match 123 | case .index(_): 124 | guard let item = statusItem(for: ident) else { return nil } 125 | 126 | return menuBarWindows.first { window in 127 | guard let statusItem = window.fetchStatusItem() else { return false } 128 | return item == statusItem 129 | } 130 | } 131 | } 132 | } 133 | 134 | // MARK: - Observers 135 | 136 | @MainActor 137 | extension MenuBarExtraUtils { 138 | /// Call from MenuBarExtraAccess init to set up observer. 139 | static func newStatusItemButtonStateObserver( 140 | index: Int, 141 | _ handler: @MainActor @escaping @Sendable (_ change: NSKeyValueObservedChange) -> Void 142 | ) -> NSStatusItem.ButtonStateObserver? { 143 | guard let statusItem = MenuBarExtraUtils.statusItem(for: .index(index)) else { 144 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 145 | print("Can't register menu bar extra state observer: Can't find status item. It may not yet exist.") 146 | #endif 147 | 148 | return nil 149 | } 150 | 151 | guard let observer = statusItem.stateObserverMenuBased(handler) 152 | else { 153 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 154 | print("Can't register menu bar extra state observer: Can't generate observer.") 155 | #endif 156 | 157 | return nil 158 | } 159 | 160 | return observer 161 | } 162 | 163 | /// Adds global event monitor to catch mouse events outside the application. 164 | static func newGlobalMouseDownEventsMonitor( 165 | _ handler: @escaping @Sendable (NSEvent) -> Void 166 | ) -> Any? { 167 | NSEvent.addGlobalMonitorForEvents( 168 | matching: [ 169 | .leftMouseDown, 170 | .rightMouseDown, 171 | .otherMouseDown 172 | ], 173 | handler: handler 174 | ) 175 | } 176 | 177 | /// Adds local event monitor to catch mouse events within the application. 178 | static func newLocalMouseDownEventsMonitor( 179 | _ handler: @escaping @Sendable (NSEvent) -> NSEvent? 180 | ) -> Any? { 181 | NSEvent.addLocalMonitorForEvents( 182 | matching: [ 183 | .leftMouseDown, 184 | .rightMouseDown, 185 | .otherMouseDown 186 | ], 187 | handler: handler 188 | ) 189 | } 190 | 191 | static func newStatusItemButtonStatePublisher( 192 | index: Int 193 | ) -> NSStatusItem.ButtonStatePublisher? { 194 | guard let statusItem = MenuBarExtraUtils.statusItem(for: .index(index)) else { 195 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 196 | print("Can't register menu bar extra state observer: Can't find status item. It may not yet exist.") 197 | #endif 198 | 199 | return nil 200 | } 201 | 202 | guard let publisher = statusItem.buttonStatePublisher() 203 | else { 204 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 205 | print("Can't register menu bar extra state observer: Can't generate publisher.") 206 | #endif 207 | 208 | return nil 209 | } 210 | 211 | return publisher 212 | } 213 | 214 | /// Wraps `newStatusItemButtonStatePublisher` in a sink. 215 | static func newStatusItemButtonStatePublisherSink( 216 | index: Int, 217 | block: @MainActor @escaping @Sendable (_ newValue: NSControl.StateValue?) -> Void 218 | ) -> AnyCancellable? { 219 | newStatusItemButtonStatePublisher(index: index)? 220 | .flatMap { value in 221 | Just(value) 222 | .tryMap { value throws -> NSControl.StateValue in value } 223 | .replaceError(with: nil) 224 | } 225 | .sink(receiveValue: { value in 226 | block(value) 227 | }) 228 | } 229 | 230 | static func newWindowObserver( 231 | index: Int, 232 | for notification: Notification.Name, 233 | block: @MainActor @escaping @Sendable (_ window: NSWindow) -> Void 234 | ) -> AnyCancellable? { 235 | NotificationCenter.default.publisher(for: notification) 236 | .filter { output in 237 | guard let window = output.object as? NSWindow else { return false } 238 | guard let windowWithIndex = MenuBarExtraUtils.window(for: .index(index)) else { return false } 239 | return window == windowWithIndex 240 | } 241 | .sink { output in 242 | guard let window = output.object as? NSWindow else { return } 243 | block(window) 244 | } 245 | } 246 | } 247 | 248 | // MARK: - NSStatusItem Introspection 249 | 250 | @MainActor 251 | extension NSStatusItem { 252 | var menuBarExtraIndex: Int { 253 | MenuBarExtraUtils.statusItems.firstIndex(of: self) ?? 0 254 | } 255 | 256 | /// Returns the ID string for the status item. 257 | /// Returns `nil` if the status item does not contain a `MacControlCenterMenu` 258 | fileprivate var menuBarExtraID: String? { 259 | // Note: this is not ideal, but it's currently the ONLY way to achieve this 260 | // until Apple adds a 1st-party solution to MenuBarExtra state 261 | 262 | // dump(statusItem.button!.target): 263 | // ▿ some: SwiftUI.WindowMenuBarExtraBehavior #0 264 | // ▿ super: SwiftUI.MenuBarExtraBehavior 265 | // - statusItem: #1 266 | // ▿ configuration: SwiftUI.MenuBarExtraConfiguration 267 | // ▿ label: SwiftUI.AnyView ---> contains the status item label, icon 268 | // ▿ mainContent: SwiftUI.AnyView ---> contains the view content 269 | // - storage 270 | // - view 271 | // - ... ---> properties of the view will be itemized here 272 | // - shouldQuitWhenRemoved: true 273 | // ▿ _isInserted: SwiftUI.Binding ---> isInserted binding backing 274 | // - isMenuBased: false 275 | // - implicitID: "YourApp.Menu" 276 | // - resizability: SwiftUI.WindowResizability.Role.automatic 277 | // - defaultSize: nil 278 | // ▿ environment: [] ---> SwiftUI environment vars/locale/openWindow method 279 | 280 | // this may require a less brittle solution if the child path may change, such as grabbing 281 | // String(dump: behavior) and using RegEx to find the value 282 | 283 | guard let behavior = button?.target, // SwiftUI.WindowMenuBarExtraBehavior <- internal 284 | let mirror = Mirror(reflecting: behavior).superclassMirror 285 | else { 286 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 287 | print("Could not find status item's target.") 288 | #endif 289 | 290 | return nil 291 | } 292 | 293 | return mirror.menuBarExtraID() 294 | } 295 | 296 | var isMenuBarExtraMenuBased: Bool { 297 | // if window-based, target will be the internal type SwiftUI.WindowMenuBarExtraBehavior 298 | // if menu-based, target will be nil 299 | guard let behavior = button?.target 300 | else { 301 | return true 302 | } 303 | 304 | // the rest of this is probably redundant given the check above covers both scenarios. 305 | // however, WindowMenuBarExtraBehavior does contain an explicit `isMenuBased` Bool we can read 306 | guard let mirror = Mirror(reflecting: behavior).superclassMirror 307 | else { 308 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 309 | print("Could not find status item's target.") 310 | #endif 311 | 312 | return false 313 | } 314 | 315 | return mirror.isMenuBarExtraMenuBased() 316 | } 317 | } 318 | 319 | // MARK: - NSWindow Introspection 320 | 321 | @MainActor 322 | extension NSWindow { 323 | fileprivate var menuBarExtraID: String? { 324 | // Note: this is not ideal, but it's currently the ONLY way to achieve this 325 | // until Apple adds a 1st-party solution to MenuBarExtra state 326 | 327 | let mirror = Mirror(reflecting: self) 328 | return mirror.menuBarExtraID() 329 | } 330 | } 331 | 332 | @MainActor 333 | extension Mirror { 334 | fileprivate func menuBarExtraID() -> String? { 335 | // Note: this is not ideal, but it's currently the ONLY way to achieve this 336 | // until Apple adds a 1st-party solution to MenuBarExtra state 337 | 338 | // this may require a less brittle solution if the child path may change, such as grabbing 339 | // String(dump: behavior) and using RegEx to find the value 340 | 341 | // when using MenuBarExtra(title string, content) this is the mirror path: 342 | if let id = descendant( 343 | "configuration", 344 | "label", 345 | "storage", 346 | "view", 347 | "content", 348 | "title", 349 | "storage", 350 | "anyTextStorage", 351 | "key", 352 | "key" 353 | ) as? String { 354 | return id 355 | } 356 | 357 | // this won't work. it differs when checked from MenuBarExtraAccess / menuBarExtraAccess(isPresented:) 358 | // internals. MenuBarExtra wraps the label in additional modifiers/AnyView here. 359 | // 360 | // otherwise, when using a MenuBarExtra initializer that produces Label view content: 361 | // we'll basically grab the hashed contents of the label 362 | if let anyView = descendant( 363 | "configuration", 364 | "label" 365 | ) as? any View { 366 | let hashed = MenuBarExtraUtils.hash(anyView: anyView) 367 | print("hash:", hashed) 368 | return hashed 369 | } 370 | 371 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 372 | print("Could not determine MenuBarExtra ID") 373 | #endif 374 | 375 | return nil 376 | } 377 | 378 | fileprivate func isMenuBarExtraMenuBased() -> Bool { 379 | descendant( 380 | "configuration", 381 | "isMenuBased" 382 | ) as? Bool ?? false 383 | } 384 | } 385 | 386 | // MARK: - Misc. 387 | 388 | @MainActor 389 | extension MenuBarExtraUtils { 390 | static func hash(anyView: any View) -> String { 391 | // can't hash `any View` 392 | // 393 | // var h = Hasher() 394 | // let ah = AnyHashable(anyView) 395 | // h.combine(anyView) 396 | // let i = h.finalize() 397 | // return String(i) 398 | 399 | // return "\(anyView)" 400 | return String("\(anyView)".hashValue) 401 | } 402 | } 403 | 404 | #endif 405 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/MenuBarExtraUtils/StatusItemIdentity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuBarExtraUtils.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(macOS) 8 | 9 | enum StatusItemIdentity: Equatable, Hashable { 10 | case index(Int) 11 | case id(String) 12 | } 13 | 14 | extension StatusItemIdentity: CustomStringConvertible { 15 | var description: String { 16 | switch self { 17 | case .index(let int): 18 | return "index \(int)" 19 | case .id(let string): 20 | return "ID \"\(string)\"" 21 | } 22 | } 23 | } 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/NSControl Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSControl Extensions.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(macOS) 8 | 9 | import AppKit 10 | 11 | extension NSControl.StateValue { 12 | @_disfavoredOverload 13 | public var name: String { 14 | switch self { 15 | case .on: 16 | return "on" 17 | case .off: 18 | return "off" 19 | case .mixed: 20 | return "mixed" 21 | default: 22 | return "unknown" 23 | } 24 | } 25 | } 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/NSEvent Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSEvent Extensions.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(macOS) 8 | 9 | import AppKit 10 | 11 | extension NSEvent.EventType { 12 | @_disfavoredOverload 13 | public var name: String { 14 | switch self { 15 | case .leftMouseDown: return "leftMouseDown" 16 | case .leftMouseUp: return "leftMouseUp" 17 | case .rightMouseDown: return "rightMouseDown" 18 | case .rightMouseUp: return "rightMouseUp" 19 | case .mouseMoved: return "mouseMoved" 20 | case .leftMouseDragged: return "leftMouseDragged" 21 | case .rightMouseDragged: return "rightMouseDragged" 22 | case .mouseEntered: return "mouseEntered" 23 | case .mouseExited: return "mouseExited" 24 | case .keyDown: return "keyDown" 25 | case .keyUp: return "keyUp" 26 | case .flagsChanged: return "flagsChanged" 27 | case .appKitDefined: return "appKitDefined" 28 | case .systemDefined: return "systemDefined" 29 | case .applicationDefined: return "applicationDefined" 30 | case .periodic: return "periodic" 31 | case .cursorUpdate: return "cursorUpdate" 32 | case .scrollWheel: return "scrollWheel" 33 | case .tabletPoint: return "tabletPoint" 34 | case .tabletProximity: return "tabletProximity" 35 | case .otherMouseDown: return "otherMouseDown" 36 | case .otherMouseUp: return "otherMouseUp" 37 | case .otherMouseDragged: return "otherMouseDragged" 38 | case .gesture: return "gesture" 39 | case .magnify: return "magnify" 40 | case .swipe: return "swipe" 41 | case .rotate: return "rotate" 42 | case .beginGesture: return "beginGesture" 43 | case .endGesture: return "endGesture" 44 | case .smartMagnify: return "smartMagnify" 45 | case .quickLook: return "quickLook" 46 | case .pressure: return "pressure" 47 | case .directTouch: return "directTouch" 48 | case .changeMode: return "changeMode" 49 | @unknown default: 50 | assertionFailure("Unhandled `NSEvent.EventType` case with raw value: \(rawValue)") 51 | return "\(rawValue)" 52 | } 53 | } 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/NSStatusItem Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSStatusItem Extensions.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(macOS) 8 | 9 | import AppKit 10 | import SwiftUI 11 | 12 | @MainActor 13 | extension NSStatusItem { 14 | /// Toggles the menu/window state by mimicking a menu item button press. 15 | @_disfavoredOverload 16 | public func togglePresented() { 17 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 18 | print("NSStatusItem.\(#function) called") 19 | #endif 20 | 21 | // this also works but only for window-based MenuBarExtra 22 | // (button.target and button.action are nil when menu-based): 23 | // - mimic user pressing the menu item button 24 | // which convinces MenuBarExtra to close the window and properly reset its state 25 | // let actionSelector = button?.action // "toggleWindow:" selector 26 | // button?.sendAction(actionSelector, to: button?.target) 27 | 28 | button?.performClick(button) 29 | updateHighlight() 30 | } 31 | 32 | /// Toggles the menu/window state by mimicking a menu item button press. 33 | @_disfavoredOverload 34 | internal func setPresented(state: Bool) { 35 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 36 | print("NSStatusItem.\(#function) called with state: \(state)") 37 | #endif 38 | 39 | // read current state and selectively call toggle if state differs 40 | let currentState = button?.state != .off 41 | guard state != currentState else { 42 | updateHighlight() 43 | return 44 | } 45 | togglePresented() 46 | } 47 | 48 | @_disfavoredOverload 49 | internal func updateHighlight() { 50 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 51 | print("NSStatusItem.\(#function) called") 52 | #endif 53 | 54 | let s = button?.state != .off 55 | 56 | #if MENUBAREXTRAACCESS_DEBUG_LOGGING 57 | print("NSStatusItem.\(#function): State detected as \(s)") 58 | #endif 59 | 60 | button?.isHighlighted = s 61 | } 62 | 63 | /// Only call this when the state of the drop-down window is known. 64 | internal func setKnownPresented(state: Bool) { 65 | switch state { 66 | case true: 67 | button?.state = .on 68 | case false: 69 | button?.state = .off 70 | } 71 | } 72 | } 73 | 74 | // MARK: - KVO Observer 75 | 76 | @MainActor 77 | extension NSStatusItem { 78 | @MainActor 79 | internal class ButtonStateObserver: NSObject { 80 | private weak var objectToObserve: NSStatusBarButton? 81 | private var observation: NSKeyValueObservation? 82 | 83 | init( 84 | object: NSStatusBarButton, 85 | _ handler: @MainActor @escaping @Sendable (_ change: NSKeyValueObservedChange) -> Void 86 | ) { 87 | objectToObserve = object 88 | super.init() 89 | 90 | observation = object.observe( 91 | \.cell!.state, 92 | options: [.initial, .new] 93 | ) { ob, change in 94 | Task { @MainActor in handler(change) } 95 | } 96 | } 97 | 98 | deinit { 99 | observation?.invalidate() 100 | } 101 | } 102 | 103 | internal func stateObserverMenuBased( 104 | _ handler: @MainActor @escaping @Sendable (_ change: NSKeyValueObservedChange) -> Void 105 | ) -> ButtonStateObserver? { 106 | guard let button else { return nil } 107 | let newStatusItemButtonStateObserver = ButtonStateObserver(object: button, handler) 108 | return newStatusItemButtonStateObserver 109 | } 110 | } 111 | 112 | // MARK: - KVO Publisher 113 | 114 | @MainActor 115 | extension NSStatusItem { 116 | typealias ButtonStatePublisher = KeyValueObservingPublisher 117 | 118 | internal func buttonStatePublisher() -> ButtonStatePublisher? { 119 | button?.publisher(for: \.cell!.state, options: [.initial, .new]) 120 | } 121 | } 122 | 123 | #endif 124 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/NSWindow Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSWindow Extensions.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(macOS) 8 | 9 | import AppKit 10 | 11 | extension NSWindow /* actually NSStatusBarWindow but it's a private AppKit type */ { 12 | /// When called on an `NSStatusBarWindow` instance, returns the associated `NSStatusItem`. 13 | /// Always returns `nil` for any other `NSWindow` subclass. 14 | @_disfavoredOverload 15 | func fetchStatusItem() -> NSStatusItem? { 16 | // statusItem is a private key not exposed to Swift but we can get it using Key-Value coding 17 | value(forKey: "statusItem") as? NSStatusItem 18 | ?? Mirror(reflecting: self).descendant("statusItem") as? NSStatusItem 19 | } 20 | } 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/Unused/Unused Code.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Unused Code.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | #if os(macOS) 8 | 9 | // import SwiftUI 10 | // import Combine 11 | // 12 | // @available(macOS 11.0, *) 13 | // extension Scene { 14 | // fileprivate func menuBarExtraID() -> String? { 15 | // // Note: this is not ideal, but it's currently the ONLY way to achieve this 16 | // // until Apple adds a 1st-party solution to MenuBarExtra state 17 | // 18 | // // this may require a less brittle solution if the child path may change, such as grabbing 19 | // // String(dump: behavior) and using RegEx to find the value 20 | // 21 | // let m = Mirror(reflecting: self) 22 | // 23 | // // TODO: detect if style is .menu or .window 24 | // 25 | // // when using MenuBarExtra(title string, content) this is the mirror path: 26 | // if let id = m.descendant( 27 | // "label", 28 | // "title", 29 | // "storage", 30 | // "anyTextStorage", 31 | // "key", 32 | // "key" 33 | // ) as? String { 34 | // return id 35 | // } 36 | // 37 | // // this won't work. it differs when checked from NSStatusItem.menuBarExtraID 38 | // // 39 | // // otherwise, when using a MenuBarExtra initializer that produces Label view content: 40 | // // we'll basically grab the hashed contents of the label 41 | // if let anyView = m.descendant( 42 | // "label" 43 | // ) as? any View { 44 | // let hashed = MenuBarExtraUtils.hash(anyView: anyView) 45 | // return hashed 46 | // } 47 | // 48 | // print("Could not determine MenuBarExtra ID") 49 | // 50 | // return nil 51 | // } 52 | // } 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /Sources/MenuBarExtraAccess/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // MenuBarExtraAccess • https://github.com/orchetect/MenuBarExtraAccess 4 | // © 2023 Steffan Andrews • Licensed under MIT License 5 | // 6 | 7 | import Foundation 8 | 9 | extension String { 10 | /// Captures the output of `dump()` for the passed object instance. 11 | init(dump object: Any) { 12 | var dumpOutput = String() 13 | dump(object, to: &dumpOutput) 14 | self = dumpOutput 15 | } 16 | } 17 | --------------------------------------------------------------------------------