├── Examples └── Sudoku │ ├── Sudoku │ ├── Preview Content │ │ └── Preview Assets.xcassets │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Sudoku.entitlements │ ├── Info.plist │ ├── main.swift │ ├── Menu Bar │ │ ├── WindowMenu.swift │ │ ├── DebugMenu.swift │ │ ├── MenuBar.swift │ │ ├── ApplicationMenu.swift │ │ ├── EditMenu.swift │ │ ├── ViewMenu.swift │ │ ├── ThemesMenu.swift │ │ └── FileMenu.swift │ ├── Cocoa-BasedTypes │ │ ├── NSEvent+Extension.swift │ │ ├── GameWindowDelegate.swift │ │ ├── NSGameWindow.swift │ │ └── NSGameHostingView.swift │ ├── Views │ │ ├── ToolTip.swift │ │ ├── Keyboard Hacks │ │ │ ├── KeyResponderGroup.swift │ │ │ └── KeyResponder.swift │ │ ├── OverlaySheets │ │ │ ├── ThemeEditor │ │ │ │ ├── ThemeEditorCellNotePreview.swift │ │ │ │ ├── ThemeSlider.swift │ │ │ │ ├── ThemeEditorCellNotesPreview.swift │ │ │ │ ├── NSViewRepresentables │ │ │ │ │ ├── PopoverPreviewBackground.swift │ │ │ │ │ ├── FontSizePopupButton.swift │ │ │ │ │ ├── ColorWell.swift │ │ │ │ │ └── FontFamilyPopupButton.swift │ │ │ │ ├── ThemeColorWell.swift │ │ │ │ ├── ThemeFontPicker.swift │ │ │ │ └── ThemeEditorCellPreview.swift │ │ │ ├── NewGameSheet.swift │ │ │ └── OKCancelRequestSheet.swift │ │ ├── CellNoteView.swift │ │ ├── SwiftUI Type Extensions │ │ │ ├── View+Extension.swift │ │ │ └── Color+Extension.swift │ │ ├── PuzzleView.swift │ │ ├── CellGroupView.swift │ │ ├── CellNotesView.swift │ │ └── ContentView.swift │ ├── Foundation Extensions │ │ ├── CGRect+Extension.swift │ │ ├── CGSize+Extension.swift │ │ └── CGPoint+Extension.swift │ └── Swift Type Extensions │ │ └── StringProtocol+Extension.swift │ ├── Sudoku.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Sudoku.xcscheme │ ├── .gitignore │ ├── SudokuTests │ ├── Info.plist │ └── SudokuTests.swift │ ├── SudokuUITests │ ├── Info.plist │ └── SudokuUITests.swift │ ├── LICENSE │ └── README.md ├── .gitignore ├── Tests ├── LinuxMain.swift └── MacMenuBarTests │ ├── XCTestManifests.swift │ └── KeyEquivalent_StringParsing_Tests.swift ├── Templates ├── install.bash ├── Project Templates │ └── Application │ │ └── App using MacMenuBar.xctemplate │ │ ├── TemplateIcon.png │ │ ├── TemplateIcon@2x.png │ │ ├── main.swift │ │ ├── DebugMenu.swift │ │ ├── App.swift │ │ ├── ContentView.swift │ │ ├── ___PACKAGENAMEASIDENTIFIER___.xcdatamodeld │ │ ├── .xccurrentversion │ │ └── ___PACKAGENAMEASIDENTIFIER___.xcdatamodel │ │ │ └── contents │ │ ├── App-CoreData.swift │ │ ├── WindowMenu.swift │ │ ├── MenuBar.swift │ │ ├── README.md │ │ ├── FileMenu.swift │ │ ├── ApplicationMenu.swift │ │ ├── ViewMenu.swift │ │ ├── Info.plist │ │ ├── Persistence.swift │ │ ├── AppDelegate.swift │ │ ├── ContentView-CoreData.swift │ │ ├── EditMenu.swift │ │ └── FormatMenu.swift └── README.md ├── TODO.md ├── LICENSE ├── Package.swift ├── Sources └── MacMenuBar │ ├── Actions │ ├── ActionResponder.swift │ ├── NoAction.swift │ ├── NSObject+Extension.swift │ ├── ClosureAction.swift │ ├── SelectorAction.swift │ └── Action.swift │ ├── MacMenus │ ├── MenuBuilder.swift │ ├── NoMenu.swift │ ├── MenuItemGroup.swift │ ├── StandardMenu.swift │ └── MacMenu.swift │ ├── MenuElement.swift │ ├── MenuBar │ └── MenuBar.swift │ ├── MacMenuItems │ ├── MenuSeparator.swift │ ├── ForEach.swift │ ├── MacMenuItem.swift │ ├── TextMenuItem.swift │ └── ActionableMenuItem.swift │ └── AppKit Subclasses │ ├── NSMenuItem+Extension.swift │ ├── NSApplicationDelegate+Extension.swift │ ├── NSMenu+Extension.swift │ └── DynamicNSMenuContent.swift └── ManualSetup.md /Examples/Sudoku/Sudoku/Preview Content/Preview Assets.xcassets: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm 7 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import MacMenuBarTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += MacMenuBarTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Templates/install.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TEMPLATE_FOLDER="${HOME}/Library/Developer/Xcode/Templates" 4 | 5 | find . -type f | grep -v install\.bash | grep -v '\.DS_Store' | cpio -pmd "${TEMPLATE_FOLDER}" 6 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chipjarred/MacMenuBar/HEAD/Templates/Project Templates/Application/App using MacMenuBar.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chipjarred/MacMenuBar/HEAD/Templates/Project Templates/Application/App using MacMenuBar.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/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 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | let delegate = AppDelegate() 3 | NSApplication.shared.delegate = delegate 4 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 5 | 6 | -------------------------------------------------------------------------------- /Tests/MacMenuBarTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(KeyEquivalent_Tests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/DebugMenu.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import MacMenuBar 3 | 4 | #if DEBUG 5 | 6 | // ------------------------------------- 7 | let debugMenu = StandardMenu(title: "Debug") 8 | { 9 | } 10 | 11 | #endif // DEBUG 12 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/App.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | @main 6 | struct ___PACKAGENAME:identifier___App: App { 7 | var body: some Scene { 8 | WindowGroup { 9 | ContentView() 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/ContentView.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | struct ContentView: View { 6 | var body: some View { 7 | Text("Hello, world!") 8 | .padding() 9 | } 10 | } 11 | 12 | struct ContentView_Previews: PreviewProvider { 13 | static var previews: some View { 14 | ContentView() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/___PACKAGENAMEASIDENTIFIER___.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | ___PACKAGENAMEASIDENTIFIER___.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/App-CoreData.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import SwiftUI 4 | 5 | @main 6 | struct ___PACKAGENAME:identifier___App: App { 7 | let persistenceController = PersistenceController.shared 8 | 9 | var body: some Scene { 10 | WindowGroup { 11 | ContentView() 12 | .environment(\.managedObjectContext, persistenceController.container.viewContext) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/WindowMenu.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MacMenuBar 3 | 4 | // ------------------------------------- 5 | let windowMenu = StandardMenu(title: "Window") 6 | { 7 | TextMenuItem(title: "Minimize", action: .minimize) 8 | TextMenuItem(title: "Zoom", action: .zoom) 9 | 10 | MenuSeparator() 11 | 12 | TextMenuItem(title: "Bring All to Front", action: .bringAllToFront) 13 | } 14 | .refuseAutoinjectedMenuItems() 15 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Sudoku.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.files.bookmarks.app-scope com.apple.security.files.bookmarks.app-scope 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | These are not in any particular order: 2 | 3 | - [ ] Update project templates for pure SwiftUI (currently uses AppDelegate) 4 | - [ ] Find a way to support usage in Catalyst apps 5 | - [ ] Support Status Menus (Menu Bar apps) 6 | - [ ] Support menu items containing controls (sliders, color wells, etc...) 7 | - [ ] Support menu items containing images 8 | - [ ] Support menu items containing attributed strings 9 | - [ ] Add images to README to illustrate the effects of the listed code. 10 | - [ ] Add tutorials for specific features 11 | - [ ] localization 12 | - [ ] dynamic menus 13 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/MenuBar.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MacMenuBar 3 | 4 | // ------------------------------------- 5 | struct MainMenuBar: MenuBar 6 | { 7 | public var body: StandardMenuBar 8 | { 9 | StandardMenuBar 10 | { 11 | applicationMenu 12 | fileMenu 13 | editMenu 14 | formatMenu 15 | viewMenu 16 | windowMenu 17 | 18 | #if DEBUG 19 | debugMenu 20 | #endif 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/README.md: -------------------------------------------------------------------------------- 1 | # Complete ___PACKAGENAME___ Project Set-up 2 | 3 | Your project is almost set up. All that's left is to add the Swift Package dependency for MacMenuBar. 4 | 5 | 1. In the `File` menu select `Swift Packages` and in the submenu `Add Dependency...` 6 | 2. In the `Choose Package Repository:` sheet that appears, type (or paste) the following URL: https://github.com/chipjarred/MacMenuBar.git 7 | 3. Click the `Next` button. 8 | 4. In the `Choose Package Options:` sheet, click `Next` again. 9 | 5. In the `Add Package to ___PACKAGENAME___` sheet, click `Finish`. 10 | 11 | -------------------------------------------------------------------------------- /Examples/Sudoku/.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | 28 | # Xcode 29 | # 30 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 31 | xcdebugger/ 32 | 33 | ## User settings 34 | xcuserdata/ 35 | 36 | ## Swift Package Manager 37 | swiftpm/ 38 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/FileMenu.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MacMenuBar 3 | 4 | // ------------------------------------- 5 | let fileMenu = StandardMenu(title: "File") 6 | { 7 | TextMenuItem(title: "New", action: .new) 8 | TextMenuItem(title: "Open...", action: .open) 9 | StandardMenu(title: "Open Recent...") 10 | 11 | MenuSeparator() 12 | 13 | TextMenuItem(title: "Close", action: .close) 14 | TextMenuItem(title: "Save...", action: .save) 15 | TextMenuItem(title: "Save As...", action: .saveAs) 16 | TextMenuItem(title: "Revert to Saved", action: .revert) 17 | 18 | MenuSeparator() 19 | 20 | TextMenuItem(title: "Page Setup...", action: .pageSetup) 21 | TextMenuItem(title: "Print", action: .print) 22 | } 23 | -------------------------------------------------------------------------------- /Examples/Sudoku/SudokuTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Examples/Sudoku/SudokuUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/___PACKAGENAMEASIDENTIFIER___.xcdatamodeld/___PACKAGENAMEASIDENTIFIER___.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/ApplicationMenu.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import MacMenuBar 3 | 4 | // ------------------------------------- 5 | let applicationMenu = StandardMenu(title: "$(AppName)") 6 | { 7 | TextMenuItem(title: "About $(AppName)", action: .about) 8 | 9 | MenuSeparator() 10 | 11 | TextMenuItem(title: "Preferences...", keyEquivalent: .command + ",") 12 | { _ in } 13 | 14 | MenuSeparator() 15 | 16 | StandardMenu(title: "Services") 17 | 18 | MenuSeparator() 19 | 20 | TextMenuItem(title: "Hide $(AppName)", action: .hide) 21 | TextMenuItem(title: "Hide Others", action: .hideOthers) 22 | TextMenuItem(title: "ShowAll", action: .showAll) 23 | 24 | MenuSeparator() 25 | 26 | TextMenuItem(title: "Quit $(AppName)", action: .quit) 27 | } 28 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/ViewMenu.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import MacMenuBar 3 | 4 | // ------------------------------------- 5 | let viewMenu = StandardMenu(title: "View") 6 | { 7 | TextMenuItem(title: "Show Toolbar", action: .showToolbar) 8 | TextMenuItem(title: "Customize Toolbar...", action: .customizeToolbar) 9 | 10 | MenuSeparator() 11 | 12 | TextMenuItem(title: "Show Sidebar", action: .showSidebar) 13 | 14 | TextMenuItem(title: "Enter Full Screen", action: .enterFullScreen) 15 | .afterAction 16 | { menuItem in 17 | if AppDelegate.isFullScreen 18 | { 19 | menuItem.title = "Exit Full Screen" 20 | KeyEquivalent.escape.set(in: menuItem) 21 | } 22 | else 23 | { 24 | menuItem.title = "Enter Full Screen" 25 | (.command + .control + "f").set(in: menuItem) 26 | } 27 | } 28 | } 29 | .refuseAutoinjectedMenuItems() 30 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSPrincipalClass 26 | NSApplication 27 | 28 | 29 | -------------------------------------------------------------------------------- /Examples/Sudoku/SudokuTests/SudokuTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SudokuTests.swift 3 | // SudokuTests 4 | // 5 | // Created by Chip Jarred on 3/19/21. 6 | // 7 | 8 | import XCTest 9 | @testable import Sudoku 10 | 11 | class SudokuTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSPrincipalClass 26 | NSApplication 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 chipjarred 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 | -------------------------------------------------------------------------------- /Examples/Sudoku/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 chipjarred 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.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MacMenuBar", 8 | platforms: [.macOS(.v10_12)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "MacMenuBar", 13 | targets: ["MacMenuBar"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "MacMenuBar", 24 | dependencies: []), 25 | .testTarget( 26 | name: "MacMenuBarTests", 27 | dependencies: ["MacMenuBar"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/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 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/main.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | let delegate = AppDelegate() 24 | NSApplication.shared.delegate = delegate 25 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 26 | 27 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/Actions/ActionResponder.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public protocol ActionResponder 25 | { 26 | func responds(to action: Action) -> Bool 27 | func performAction(_ action: Action, for sender: Any?) 28 | } 29 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Menu Bar/WindowMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | import MacMenuBar 23 | 24 | // ------------------------------------- 25 | let windowMenu = StandardMenu(title: "Window") 26 | { 27 | TextMenuItem(title: "Minimize", action: .minimize) 28 | TextMenuItem(title: "Zoom", action: .zoom) 29 | } 30 | .refuseAutoinjectedMenuItems() 31 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Menu Bar/DebugMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | import MacMenuBar 23 | 24 | #if DEBUG 25 | 26 | // ------------------------------------- 27 | let debugMenu = StandardMenu(title: "Debug") 28 | { 29 | TextMenuItem(title: "Reset UserDefaults") 30 | {_ in 31 | Preferences.shared.reset() 32 | AppDelegate.shared.savePreferences() 33 | } 34 | } 35 | 36 | #endif // DEBUG 37 | -------------------------------------------------------------------------------- /Examples/Sudoku/SudokuUITests/SudokuUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SudokuUITests.swift 3 | // SudokuUITests 4 | // 5 | // Created by Chip Jarred on 3/19/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class SudokuUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenus/MenuBuilder.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | // ------------------------------------- 22 | @_functionBuilder 23 | public struct MenuBuilder 24 | { 25 | // ------------------------------------- 26 | public static func buildBlock() -> [MenuElement] { 27 | return [] 28 | } 29 | 30 | // ------------------------------------- 31 | public static func buildBlock( 32 | _ items: MenuElement...) -> [MenuElement] 33 | { 34 | return items 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Cocoa-BasedTypes/NSEvent+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import AppKit 22 | 23 | extension NSEvent 24 | { 25 | var char: Character? { 26 | return characters?.first 27 | } 28 | 29 | func matches(for button: NSButton) -> Bool 30 | { 31 | return button.keyEquivalent == self.characters 32 | && button.keyEquivalentModifierMask == 33 | button.keyEquivalentModifierMask.intersection( 34 | self.modifierFlags) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Menu Bar/MenuBar.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | import MacMenuBar 23 | 24 | // ------------------------------------- 25 | struct MainMenuBar: MenuBar 26 | { 27 | public var body: StandardMenuBar 28 | { 29 | StandardMenuBar 30 | { 31 | applicationMenu 32 | fileMenu 33 | editMenu 34 | themesMenu 35 | viewMenu 36 | windowMenu 37 | 38 | #if DEBUG 39 | debugMenu 40 | #endif 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Cocoa-BasedTypes/GameWindowDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | @objc class GameWindowDelegate: NSObject, NSWindowDelegate 25 | { 26 | unowned var window: NSWindow 27 | var firstUpdate = true 28 | 29 | // ------------------------------------- 30 | init(_ window: NSWindow) { 31 | self.window = window 32 | } 33 | 34 | // ------------------------------------- 35 | public func windowWillClose(_ notification: Notification) { 36 | NSApp.terminate(self) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/ToolTip.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | struct Tooltip: NSViewRepresentable 25 | { 26 | let tooltip: String 27 | 28 | // ------------------------------------- 29 | func makeNSView(context: NSViewRepresentableContext) -> NSView 30 | { 31 | let view = NSView() 32 | view.toolTip = tooltip 33 | 34 | return view 35 | } 36 | 37 | func updateNSView( 38 | _ nsView: NSView, 39 | context: NSViewRepresentableContext) 40 | { } 41 | } 42 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Menu Bar/ApplicationMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | import MacMenuBar 23 | 24 | // ------------------------------------- 25 | let applicationMenu = StandardMenu(title: "$(AppName)") 26 | { 27 | TextMenuItem(title: "About $(AppName)", action: .about) 28 | 29 | MenuSeparator() 30 | 31 | TextMenuItem(title: "Preferences...", keyEquivalent: .command + ",") 32 | { _ in } 33 | .enabled(false) 34 | 35 | MenuSeparator() 36 | 37 | TextMenuItem(title: "Hide $(AppName)", action: .hide) 38 | TextMenuItem(title: "Hide Others", action: .hideOthers) 39 | TextMenuItem(title: "ShowAll", action: .showAll) 40 | 41 | MenuSeparator() 42 | 43 | TextMenuItem(title: "Quit $(AppName)", action: .quit) 44 | } 45 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/Keyboard Hacks/KeyResponderGroup.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | /** 25 | Sets a new key responder when creating the group, and automatically resigns 26 | it when the group disappears. 27 | */ 28 | struct KeyResponderGroup: View 29 | { 30 | let content: () -> Content 31 | 32 | // ------------------------------------- 33 | init(content: @escaping () -> Content) { 34 | self.content = content 35 | } 36 | 37 | // ------------------------------------- 38 | var body: some View 39 | { 40 | let keyResponder = KeyResponder() 41 | keyResponder.defaultHandler() 42 | return Group { content() }.onDisappear { keyResponder.resign() } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Examples/Sudoku/README.md: -------------------------------------------------------------------------------- 1 | # MacMenuBar Example Project: Sudoku 2 | 3 | *Note: This example app is a work in progress, but it's finished enough for the purposes of seeing MacMenuBar in use, so I decided not to wait to make it available.* 4 | 5 | This project demonstrates how to use MacMenuBar to manage menus in a SwiftUI application, in this case, a sudoku game. Although I do promise that the puzzles it randomly produces are valid sudoku puzzles, I don't promise that they're especially well designed by sudoku standards, considering that the main point was to demonstrate MacMenuBar in a real application. That said, it is playable in ways that I think a user would expect, including keyboard navigation in the puzzle as well as mouse. 6 | 7 | Although working with the menu bar is a weak point in SwiftUI for macOS, and MacMenuBar handles that effectively, it's not the only deficiency in SwiftUI on Mac. Keyboard handling is another. To implement keyboard navigation in the puzzle, this project uses some Cocoa-based hacks that work well for a single window application, but wouldn't be appropriate for a multi-window, document-based app. Those hacks are just for navigation in the views though. MacMenuBar does an effective job handling the menus, including key equivalents. 8 | 9 | This project uses: 10 | 11 | - Selector-based menu actions 12 | - Closure-based menu actions 13 | - Dynamically enabled and disabled menu items. 14 | - Dynamically stateful menu items (checked vs. unchecked) 15 | - Dynamically populated menus 16 | - Dynamically renamed menu items 17 | 18 | ## TODO 19 | - [x] Undo/Redo 20 | - [x] Game save/restore 21 | - [x] Revert from saved 22 | - [x] Edit/Create themes 23 | - [x] Save preferences 24 | - [ ] Puzzle solved view 25 | - [ ] Usage tutorial on first run 26 | - [ ] Keyboard navigation guides 27 | - [ ] Modify Guess/Note View to change when option key is pressed. 28 | - [x] Support option-click for bringing up Note view 29 | - [ ] Support long click for bringing up Guess view. 30 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Menu Bar/EditMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | import MacMenuBar 23 | 24 | var undoMenuItem = TextMenuItem(title: "Undo", action: .undo) 25 | .enabledWhen { PuzzleObject.shared.canUndo } 26 | var redoMenuItem = TextMenuItem(title: "Redo", action: .redo) 27 | .enabledWhen { PuzzleObject.shared.canRedo } 28 | 29 | // ------------------------------------- 30 | let editMenu = StandardMenu(title: "Edit") 31 | { 32 | undoMenuItem 33 | redoMenuItem 34 | 35 | MenuSeparator() 36 | 37 | TextMenuItem(title: "Cut", action: .cut) 38 | TextMenuItem(title: "Copy", action: .copy) 39 | TextMenuItem(title: "Paste", action: .paste) 40 | TextMenuItem(title: "Delete", action: .delete) 41 | 42 | MenuSeparator() 43 | 44 | TextMenuItem(title: "Select All", action: .selectAll) 45 | } 46 | -------------------------------------------------------------------------------- /Templates/README.md: -------------------------------------------------------------------------------- 1 | # Xcode Templates for MacMenuBar 2 | 3 | This folder contains Xcode templates that set up a new projects that already already configured to use MacMenuBar. 4 | 5 | The only thing you have to do is to add the package dependency on MacMenuBar in `Swift Packages` menu in Xcode's `File` menu. 6 | 7 | *I would love to configure the templates so that even manually adding the package dependency is unnecessary, but have yet to find a way to do it. If you have figured out how to configure package dependencies in a project template, please let me know!* 8 | 9 | ### Template Descriptions 10 | 11 | - `App using MacMenuBar`: Creates the same starter app as the one supplied by Apple's Xcode template, but with all of the main menu items implemented using MacMenuBar instead of a Storyboard. 12 | 13 | ## Installing the Templates 14 | 15 | You can install the templates automatically using the `install.bash` provided in this directory, or manually. 16 | 17 | ### Install Using `install.bash` 18 | 19 | 1. Open `Terminal` 20 | 2. Change directories to this directory. For example, if you cloned `MacMenuBar` into a `Packages` directory in your home directory, you'd type 21 | ```bash 22 | cd ~/Pacakges/MacMenuBar/Templates 23 | ``` 24 | 3. Run `install.bash` 25 | ```bash 26 | ./install.bash 27 | ``` 28 | If you get an error that you can't execute the script, you just need to set its executable bit first: 29 | ```bash 30 | chmod -x install.bash 31 | ./install.bash 32 | ``` 33 | 34 | ### Manual Installation 35 | 1. In Finder press command-shift-G. Then type `~/Library/Developer/Xcode`. 36 | 2. If there is no `Templates` folder, create it. 37 | 3. Open the `Templates` folder 38 | 4. If folders named `File Templates` and `Project Templates` are missing, create them. 39 | 5. Open the `Project Templates` folder. 40 | 6. If there there is no `Applicaton` folder, create it. 41 | 7. While holding down the option key, drag the templates you want to install from within the package's `Templates` subfolders into the `Application` folder from step 6. 42 | 43 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Cocoa-BasedTypes/NSGameWindow.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | @objc class NSGameWindow: NSWindow 25 | { 26 | // ------------------------------------- 27 | override init( 28 | contentRect: NSRect, 29 | styleMask style: NSWindow.StyleMask, 30 | backing backingStoreType: NSWindow.BackingStoreType, 31 | defer flag: Bool) 32 | { 33 | super.init( 34 | contentRect: contentRect, 35 | styleMask: style, 36 | backing: backingStoreType, 37 | defer: flag 38 | ) 39 | } 40 | 41 | // ------------------------------------- 42 | @objc override func keyDown(with event: NSEvent) 43 | { 44 | if (contentView as? NSGameHostingView)?.keyDown(with: event) == false { 45 | super.keyDown(with: event) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MenuElement.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | 23 | // ------------------------------------- 24 | public protocol MenuElement 25 | { 26 | var isItem: Bool { get } 27 | var title: String { get set } 28 | var isVisible: Bool { get set } 29 | var canBeEnabled: Bool { get set } 30 | var isEnabled: Bool { get } 31 | 32 | func appendSelf(to menu: inout T) 33 | } 34 | 35 | // ------------------------------------- 36 | public extension MenuElement 37 | { 38 | // ------------------------------------- 39 | func enable(_ value: Bool = true) -> Self 40 | { 41 | var copy = self 42 | copy.canBeEnabled = value 43 | return copy 44 | } 45 | 46 | // ------------------------------------- 47 | func visible(_ value: Bool = true) -> Self 48 | { 49 | var copy = self 50 | copy.isVisible = value 51 | return copy 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MenuBar/MenuBar.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public protocol MenuBar 25 | { 26 | var body: StandardMenuBar { get } 27 | } 28 | 29 | // ------------------------------------- 30 | @_functionBuilder 31 | public struct MenuBarBuilder { 32 | public static func buildBlock(_ menus: MacMenu...) -> [MacMenu] { menus } 33 | } 34 | 35 | // ------------------------------------- 36 | public struct StandardMenuBar 37 | { 38 | internal private(set) var menu: NSMenu 39 | 40 | public init(@MenuBarBuilder menus: () -> [MacMenu]) 41 | { 42 | self.menu = NSMenu() 43 | menus().forEach 44 | { 45 | if $0 is NoMenu { return } 46 | 47 | let item = NSMenuItem() 48 | item.title = $0.title 49 | item.submenu = $0.nsMenu 50 | self.menu.addItem(item) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/ThemeEditorCellNotePreview.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | // 21 | 22 | import SwiftUI 23 | 24 | // ------------------------------------- 25 | struct ThemeEditorCellNotePreview: View 26 | { 27 | static let width = CellNoteView.width 28 | static let height = CellNoteView.height 29 | 30 | var note: Int? 31 | 32 | @Binding var currentTheme: Theme 33 | 34 | // ------------------------------------- 35 | var displayText: Text 36 | { 37 | let s: String 38 | if let n = note { s = "\(n)" } 39 | else { s = "" } 40 | 41 | return Text(s) 42 | } 43 | 44 | // ------------------------------------- 45 | var body: some View 46 | { 47 | displayText 48 | .font(Font(currentTheme.noteFont)) 49 | .foregroundColor(Color(currentTheme.noteColor)) 50 | .frame(width: Self.width, height: Self.height, alignment: .center) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenuItems/MenuSeparator.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public struct MenuSeparator 25 | { 26 | public private(set) var nsMenuItem: NSMenuItem = NSMacMenuItem.separator() 27 | 28 | public init() { } 29 | } 30 | 31 | // ------------------------------------- 32 | extension MenuSeparator: MacMenuItem 33 | { 34 | // ------------------------------------- 35 | public var title: String 36 | { 37 | get { nsMenuItem.title } 38 | set { } 39 | } 40 | 41 | // ------------------------------------- 42 | public var isVisible: Bool 43 | { 44 | get { true } 45 | set { } 46 | } 47 | 48 | // ------------------------------------- 49 | public var canBeEnabled: Bool 50 | { 51 | get { false } 52 | set { } 53 | } 54 | 55 | // ------------------------------------- 56 | public var isEnabled: Bool { false } 57 | } 58 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/NewGameSheet.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | struct NewGameRequestSheet: OKCancelRequestSheet 25 | { 26 | let keyResponder = KeyResponder() 27 | 28 | let okText = "New Game" 29 | let cancelText: String? = "Cancel" 30 | let title = "Start New Game?" 31 | let description = 32 | "Would you like to resign the current game to start the new one?" 33 | 34 | @EnvironmentObject var sheetRequest: SheetRequest 35 | @EnvironmentObject var puzzle: PuzzleObject 36 | 37 | // ------------------------------------- 38 | func cancel() { sheetRequest.state = .none } 39 | 40 | // ------------------------------------- 41 | func ok() 42 | { 43 | puzzle.newPuzzle() 44 | sheetRequest.state = .none 45 | } 46 | } 47 | 48 | struct NewGameSheet_Previews: PreviewProvider { 49 | static var previews: some View { 50 | NewGameRequestSheet() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/ThemeSlider.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | // 21 | 22 | import SwiftUI 23 | 24 | // ------------------------------------- 25 | struct ThemeSlider: View 26 | where FloatValue.Stride: BinaryFloatingPoint 27 | { 28 | var label: String 29 | @State var sliderValue: FloatValue 30 | @Binding var currentTheme: Theme 31 | let keyPath: WritableKeyPath 32 | 33 | // ------------------------------------- 34 | var body: some View 35 | { 36 | HStack(spacing: 0) 37 | { 38 | Text("\(label):") 39 | .font(.system(size: 10)) 40 | .foregroundColor(.controlTextColor) 41 | .frame(alignment: .trailing) 42 | .padding([.leading, .trailing], 5) 43 | Slider(value: $sliderValue, in: 0.0...1.0) { _ in 44 | currentTheme[keyPath: keyPath] = sliderValue 45 | }.frame(width: 100) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/Actions/NoAction.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | 23 | // ------------------------------------- 24 | public struct NoAction: Action 25 | { 26 | public init() { } 27 | 28 | // ------------------------------------- 29 | public var keyEquivalent: KeyEquivalent? 30 | { 31 | get { nil } 32 | set { } 33 | } 34 | 35 | // ------------------------------------- 36 | public var canBeEnabled: Bool 37 | { 38 | get { false } 39 | set { } 40 | } 41 | 42 | // ------------------------------------- 43 | public var isEnabled: Bool { false } 44 | 45 | // ------------------------------------- 46 | public var enabledValidator: (() -> Bool)? 47 | { 48 | get { nil } 49 | set { } 50 | } 51 | 52 | // ------------------------------------- 53 | public func performActionSelf( 54 | on target: ActionResponder?, 55 | for sender: Any?) -> Bool 56 | { 57 | return false 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Menu Bar/ViewMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | import MacMenuBar 23 | 24 | // ------------------------------------- 25 | let viewMenu = StandardMenu(title: "View") 26 | { 27 | TextMenuItem(title: "Show Toolbar", action: .showToolbar).enabled(false) 28 | TextMenuItem(title: "Customize Toolbar...", action: .customizeToolbar).enabled(false) 29 | 30 | MenuSeparator() 31 | 32 | TextMenuItem(title: "Show Sidebar", action: .showSidebar).enabled(false) 33 | 34 | TextMenuItem(title: "Enter Full Screen", action: .enterFullScreen) 35 | .afterAction 36 | { menuItem in 37 | if AppDelegate.isFullScreen 38 | { 39 | menuItem.title = "Exit Full Screen" 40 | KeyEquivalent.escape.set(in: menuItem) 41 | } 42 | else 43 | { 44 | menuItem.title = "Enter Full Screen" 45 | (.command + .control + "f").set(in: menuItem) 46 | } 47 | } 48 | } 49 | .refuseAutoinjectedMenuItems() 50 | -------------------------------------------------------------------------------- /ManualSetup.md: -------------------------------------------------------------------------------- 1 | # Configuring a New Xcode Project for MacMenuBar 2 | 3 | These instructions are for configuring a new project that was created using Apple's `App` template that comes with Xcode. 4 | 5 | Since Xcode's starter project template for a macOS SwiftUI app isn't set up for `MacMenuBar` there are few things you have to change to make it ready. 6 | 7 | 1. Add `MacMenuBar` as a Swift Package Dependency in your app. In Xcode select `Swift Packages` from the `File` menu, then `Add Package Dependency`. Then fill-in the URL for this package: [https://github.com/chipjarred/MacMenuBar.git](https://github.com/chipjarred/MacMenuBar.git) 8 | 9 | 2. Remove the `Main.storyboard` file. Just like you don't use a Storyboard for your SwiftUI `View` types, you don't use them for `MacMenuBar` either. Just delete it (or uncheck it as belonging to the application target in the `File Inspector` side-bar on the right). 10 | 11 | 3. Change the `Main Interface` target setting. 12 | 1. Click on the project in the Project Navigator (side bar to the left that shows files and folder) 13 | 2. Select your application target 14 | 3. Select the "General" tab at the top, then in the "Deployment Info" section, clear the "Main Interface" field. 15 | 16 | 4. Add `main.swift`. With `Main.storyboard` gone, `NSApplication` won't work automagically, so you have to add a `main.swift` to provide a working entry point for your app. It should look like this. 17 | 18 | ```swift 19 | import Cocoa 20 | let delegate = AppDelegate() 21 | NSApplication.shared.delegate = delegate 22 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 23 | ``` 24 | 5. Remove the `@NSApplicationMain` attribute from `AppDelegate` in `AppDelegate.swift`. 25 | ```swift 26 | @NSApplicationMain // <- REMOVE THIS 27 | class AppDelegate: NSObject, NSApplicationDelegate 28 | ``` 29 | 30 | 6. Import `MacMenuBar` in `AppDelelgate.swift`: 31 | 32 | ```swift 33 | import Cocoa 34 | import SwiftUI 35 | import MacMenuBar // <-- ADD THIS 36 | ``` 37 | 38 | 7. Test the setup by building and running the app. You no longer have a menu bar in the app, so *you'll need to kill it using Stop button in Xcode.* 39 | 40 | You're now ready to start building your menus. 41 | 42 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/CellNoteView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | struct CellNoteView: View 25 | { 26 | @EnvironmentObject var prefs: Preferences 27 | 28 | static let width = CellView.width / 3 29 | static let height = width 30 | 31 | var note: Int? 32 | 33 | var displayText: Text 34 | { 35 | let s: String 36 | if let n = note { s = "\(n)" } 37 | else { s = "" } 38 | 39 | return Text(s) 40 | } 41 | 42 | // ------------------------------------- 43 | var body: some View 44 | { 45 | displayText 46 | .font(Font(prefs.theme.noteFont)) 47 | .foregroundColor(Color(prefs.theme.noteColor)) 48 | .frame(width: Self.width, height: Self.height, alignment: .center) 49 | } 50 | } 51 | 52 | // ------------------------------------- 53 | struct CellNoteView_Previews: PreviewProvider 54 | { 55 | static var previews: some View { 56 | CellNoteView(note: 5) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/Actions/NSObject+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | extension NSObject: ActionResponder 25 | { 26 | // ------------------------------------- 27 | public func responds(to action: Action) -> Bool 28 | { 29 | if let selector = (action as? SelectorAction)?.selector { 30 | return responds(to: selector) 31 | } 32 | 33 | return action is ClosureAction 34 | } 35 | 36 | // ------------------------------------- 37 | public func performAction(_ action: Action, for sender: Any?) 38 | { 39 | if let closure = (action as? ClosureAction)?.closure { 40 | return closure(sender) 41 | } 42 | 43 | guard let selector = (action as? SelectorAction)?.selector 44 | else { return } 45 | 46 | assert( 47 | responds(to: selector), 48 | "\(type(of: self)) does not respond to \(selector)" 49 | ) 50 | 51 | perform(selector, with: sender) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/AppKit Subclasses/NSMenuItem+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public extension NSMenuItem 25 | { 26 | // ------------------------------------- 27 | func setSelectorAction(_ action: StandardMenuItemAction) 28 | { 29 | let (selector, keyEquivalent) = action.selectorAndKeyEquivalent 30 | 31 | setSelectorAction( 32 | SelectorAction(keyEquivalent: keyEquivalent, selector: selector) 33 | ) 34 | } 35 | 36 | // ------------------------------------- 37 | func setSelectorAction(_ action: SelectorAction) 38 | { 39 | if let nsMacMenuItem = self as? NSMacMenuItem { 40 | nsMacMenuItem._action = action 41 | } 42 | else 43 | { 44 | self.action = action.selector 45 | self.keyEquivalent = action.keyEquivalent?.description ?? "" 46 | self.keyEquivalentModifierMask = action.keyEquivalent?.modifiers 47 | ?? NSEvent.ModifierFlags() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Foundation Extensions/CGRect+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | 23 | // ------------------------------------- 24 | public extension CGRect 25 | { 26 | // ------------------------------------- 27 | func move(to point: CGPoint) -> CGRect { 28 | CGRect(origin: point, size: size) 29 | } 30 | 31 | func translate(by delta: CGSize) -> CGRect { 32 | CGRect(origin: origin + delta, size: size) 33 | } 34 | 35 | // ------------------------------------- 36 | func inset(by delta: CGSize) -> CGRect 37 | { 38 | CGRect( 39 | origin: origin + delta, 40 | size: size - delta 41 | ) 42 | } 43 | 44 | // ------------------------------------- 45 | func inset(deltaX: CGFloat, deltaY: CGFloat) -> CGRect{ 46 | inset(by: CGSize(width: deltaX, height: deltaY)) 47 | } 48 | 49 | var topLeft: CGPoint { origin } 50 | var topRight: CGPoint { .init(x: maxX, y: minY) } 51 | var bottomLeft: CGPoint { .init(x: minX, y: maxY) } 52 | var bottomRight: CGPoint { origin + size } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenus/NoMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | // ------------------------------------- 22 | public struct NoMenu 23 | { 24 | public internal(set) var nsMenu: NSMacMenu 25 | 26 | // ------------------------------------- 27 | public init() { 28 | self.nsMenu = NSMacMenu() 29 | self.nsMenu.delegate = self.nsMenu 30 | } 31 | } 32 | 33 | // ------------------------------------- 34 | extension NoMenu: MacMenu 35 | { 36 | // ------------------------------------- 37 | @inlinable public var isItem: Bool { false } 38 | 39 | // ------------------------------------- 40 | @inlinable public var title: String 41 | { 42 | get { "" } 43 | set { } 44 | } 45 | 46 | // ------------------------------------- 47 | @inlinable public var isVisible: Bool 48 | { 49 | get { false } 50 | set { } 51 | } 52 | 53 | // ------------------------------------- 54 | @inlinable public var canBeEnabled: Bool 55 | { 56 | get { false } 57 | set { } 58 | } 59 | 60 | // ------------------------------------- 61 | @inlinable public var isEnabled: Bool { false } 62 | } 63 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/SwiftUI Type Extensions/View+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | extension View 25 | { 26 | // ------------------------------------- 27 | @inlinable 28 | func frame(_ size: CGSize) -> some View { 29 | frame(width: size.width, height: size.height) 30 | } 31 | 32 | // ------------------------------------- 33 | @inlinable 34 | func hAlign(_ alignment: HorizontalAlignment) -> some View { 35 | VStack(alignment: alignment, spacing: 0) { self } 36 | } 37 | 38 | // ------------------------------------- 39 | @inlinable 40 | func hAlign(_ alignment: VerticalAlignment) -> some View { 41 | HStack(alignment: alignment, spacing: 0) { self } 42 | } 43 | } 44 | 45 | // ------------------------------------- 46 | extension View 47 | { 48 | // ------------------------------------- 49 | func toolTip(_ toolTip: String) -> some View { 50 | self.overlay(Tooltip(tooltip: toolTip)) 51 | } 52 | 53 | // ------------------------------------- 54 | func selectable(onTap block: @escaping () -> Void) -> some View { 55 | self.onTapGesture { block() } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/ThemeEditorCellNotesPreview.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | // 21 | 22 | import SwiftUI 23 | 24 | // ------------------------------------- 25 | struct ThemeEditorCellNotesPreview: View 26 | { 27 | var notes: Set 28 | 29 | @Binding var currentTheme: Theme 30 | 31 | // ------------------------------------- 32 | func note(_ noteRow: Int, _ noteCol: Int) -> Int? 33 | { 34 | let noteValue = 3 * noteRow + noteCol + 1 35 | return notes.contains(noteValue) ? noteValue : nil 36 | } 37 | 38 | // ------------------------------------- 39 | var body: some View 40 | { 41 | VStack(spacing:0) 42 | { 43 | ForEach(0..<3) 44 | { noteRow in 45 | HStack(spacing: 0) 46 | { 47 | ForEach(0..<3) 48 | { noteCol in 49 | ThemeEditorCellNotePreview( 50 | note: note(noteRow, noteCol), 51 | currentTheme: $currentTheme 52 | ) 53 | } 54 | } 55 | .scaledToFit() 56 | } 57 | } 58 | .scaledToFit() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/AppKit Subclasses/NSApplicationDelegate+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | import SwiftUI 23 | 24 | // ------------------------------------- 25 | fileprivate func _setMenuBar(to menuBar: MenuBarType) 26 | { 27 | DispatchQueue.main.async 28 | { 29 | let menu = menuBar.body.menu 30 | // Build dynamic menu content before setting the menu bar so our menus 31 | // are populated before macOS knows about them to inject its own. 32 | for item in menu.items 33 | { 34 | if let submenu = item.submenu { 35 | let _ = submenu.delegate?.numberOfItems?(in: submenu) 36 | } 37 | } 38 | NSApplication.shared.mainMenu = menu 39 | } 40 | } 41 | 42 | // ------------------------------------- 43 | public extension SwiftUI.App 44 | { 45 | // ------------------------------------- 46 | func setMenuBar(to menuBar: MenuBarType) { 47 | _setMenuBar(to: menuBar) 48 | } 49 | } 50 | 51 | // ------------------------------------- 52 | public extension NSApplicationDelegate 53 | { 54 | func setMenuBar(to menuBar: MenuBarType) { 55 | _setMenuBar(to: menuBar) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/NSViewRepresentables/PopoverPreviewBackground.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | struct PopoverPreviewBackground: NSViewRepresentable 25 | { 26 | typealias NSViewType = CustomVisualEffectView 27 | 28 | // ------------------------------------- 29 | class CustomVisualEffectView: NSVisualEffectView 30 | { 31 | // ------------------------------------- 32 | override var allowsVibrancy: Bool { true } 33 | 34 | // ------------------------------------- 35 | override func viewDidMoveToSuperview() 36 | { 37 | super.viewDidMoveToSuperview() 38 | if let frame = superview?.frame { 39 | setFrameSize(frame.size) 40 | } 41 | } 42 | } 43 | 44 | // ------------------------------------- 45 | func makeNSView(context: Context) -> NSViewType 46 | { 47 | let view = NSViewType() 48 | view.material = .popover 49 | view.blendingMode = .withinWindow 50 | view.state = .active 51 | return view 52 | } 53 | 54 | // ------------------------------------- 55 | func updateNSView(_ nsView: NSViewType, context: Context) { } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/Actions/ClosureAction.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | 23 | // ------------------------------------- 24 | public struct ClosureAction: Action 25 | { 26 | public typealias ActionClosure = (_: Any?) -> Void 27 | 28 | public var closure: ActionClosure 29 | public var keyEquivalent: KeyEquivalent? = nil 30 | public var canBeEnabled: Bool = true 31 | public var isEnabled: Bool { 32 | return canBeEnabled && (enabledValidator?() ?? true) 33 | } 34 | public var enabledValidator: (() -> Bool)? = nil 35 | 36 | // ------------------------------------- 37 | public init(_ closure: @escaping ActionClosure) { self.closure = closure } 38 | 39 | // ------------------------------------- 40 | public init(keyEquivalent: KeyEquivalent, closure: @escaping ActionClosure) 41 | { 42 | self.init(closure) 43 | self.keyEquivalent = keyEquivalent 44 | } 45 | 46 | // ------------------------------------- 47 | /* 48 | `target` is ignored for `ClosureAction`s 49 | */ 50 | public func performActionSelf( 51 | on target: ActionResponder?, 52 | for sender: Any?) -> Bool 53 | { 54 | guard isEnabled else { return false } 55 | closure(sender) 56 | return true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/Persistence.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import CoreData 4 | 5 | struct PersistenceController { 6 | static let shared = PersistenceController() 7 | 8 | static var preview: PersistenceController = { 9 | let result = PersistenceController(inMemory: true) 10 | let viewContext = result.container.viewContext 11 | for _ in 0..<10 { 12 | let newItem = Item(context: viewContext) 13 | newItem.timestamp = Date() 14 | } 15 | do { 16 | try viewContext.save() 17 | } catch { 18 | // Replace this implementation with code to handle the error appropriately. 19 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 20 | let nsError = error as NSError 21 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 22 | } 23 | return result 24 | }() 25 | 26 | let container: ___VARIABLE_persistentContainerClass___ 27 | 28 | init(inMemory: Bool = false) { 29 | container = ___VARIABLE_persistentContainerClass___(name: "___PACKAGENAME:identifier___") 30 | if inMemory { 31 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") 32 | } 33 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 34 | if let error = error as NSError? { 35 | // Replace this implementation with code to handle the error appropriately. 36 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 37 | 38 | /* 39 | Typical reasons for an error here include: 40 | * The parent directory does not exist, cannot be created, or disallows writing. 41 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 42 | * The device is out of space. 43 | * The store could not be migrated to the current model version. 44 | Check the error message to determine what the actual problem was. 45 | */ 46 | fatalError("Unresolved error \(error), \(error.userInfo)") 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Foundation Extensions/CGSize+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | 23 | // ------------------------------------- 24 | public extension CGSize 25 | { 26 | // ------------------------------------- 27 | var transposed: CGSize { CGSize(width: height, height: width) } 28 | 29 | // ------------------------------------- 30 | static func + (lhs: CGSize, rhs: CGSize) -> CGSize 31 | { 32 | return CGSize( 33 | width: lhs.width + rhs.width, 34 | height: lhs.height + rhs.height 35 | ) 36 | } 37 | 38 | // ------------------------------------- 39 | static func - (lhs: CGSize, rhs: CGSize) -> CGSize 40 | { 41 | return CGSize( 42 | width: lhs.width - rhs.width, 43 | height: lhs.height - rhs.height 44 | ) 45 | } 46 | 47 | // ------------------------------------- 48 | static func * (lhs: CGSize, rhs: CGFloat) -> CGSize { 49 | return CGSize(width: lhs.width * rhs, height: lhs.height * rhs) 50 | } 51 | 52 | // ------------------------------------- 53 | static func * (lhs: CGFloat, rhs: CGSize) -> CGSize { 54 | return rhs * lhs 55 | } 56 | 57 | // ------------------------------------- 58 | static func / (lhs: CGSize, rhs: CGFloat) -> CGSize { 59 | return CGSize(width: lhs.width / rhs, height: lhs.height / rhs) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/PuzzleView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | 24 | // ------------------------------------- 25 | struct PuzzleView: View 26 | { 27 | @EnvironmentObject var puzzle: PuzzleObject 28 | @EnvironmentObject var prefs: Preferences 29 | 30 | static let groupSpacing: CGFloat = 3 31 | static let width = CellGroupView.width * 3 + groupSpacing * 2 32 | static let height = width 33 | 34 | // ------------------------------------- 35 | var body: some View 36 | { 37 | VStack(spacing: Self.groupSpacing) 38 | { 39 | ForEach(0..<3) 40 | { row in 41 | HStack(spacing: Self.groupSpacing) 42 | { 43 | ForEach(0..<3) 44 | { col in 45 | CellGroupView( 46 | row: row, 47 | col: col 48 | ).environmentObject(puzzle).environmentObject(prefs) 49 | } 50 | } 51 | } 52 | } 53 | .frame(width: Self.width, height: Self.height, alignment: .center) 54 | } 55 | } 56 | 57 | // ------------------------------------- 58 | struct PuzzleView_Previews: PreviewProvider 59 | { 60 | static var puzzle = previewPuzzle 61 | static var previews: some View { 62 | PuzzleView().environmentObject(Preferences()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Menu Bar/ThemesMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import MacMenuBar 22 | 23 | // ------------------------------------- 24 | fileprivate func menuState(for theme: Theme) -> MacMenuItemState { 25 | Preferences.shared.theme.name == theme.name ? .on : .off 26 | } 27 | 28 | // ------------------------------------- 29 | let themesMenu = StandardMenu(title: "Themes") 30 | { 31 | TextMenuItem(title: "System") { _ in 32 | Preferences.shared.theme = .system 33 | } 34 | .updatingStateWith { menuState(for: .system) } 35 | 36 | TextMenuItem(title: Theme.light.name) { _ in 37 | Preferences.shared.theme = .light 38 | } 39 | .updatingStateWith { menuState(for: .light) } 40 | 41 | TextMenuItem(title: Theme.dark.name) { _ in 42 | Preferences.shared.theme = .dark 43 | } 44 | .updatingStateWith { menuState(for: .dark) } 45 | 46 | MenuSeparator() 47 | 48 | TextMenuItem(title: "Custom Themes").enabled(false) 49 | ForEach(Preferences.shared.customThemes) 50 | { theme in 51 | TextMenuItem(title: theme.name) { _ in 52 | Preferences.shared.setTheme(named: theme.name) 53 | } 54 | .indented() 55 | .updatingStateWith { menuState(for: theme) } 56 | } 57 | 58 | MenuSeparator() 59 | TextMenuItem(title: "Edit Themes") { _ in 60 | AppDelegate.shared.sheetRequest.state = .editThemes 61 | }.enabledWhen { AppDelegate.shared.sheetRequest.state == .none } 62 | } 63 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | import MacMenuBar 4 | 5 | class AppDelegate: NSObject, NSApplicationDelegate { 6 | 7 | var window: NSWindow! 8 | 9 | func applicationDidFinishLaunching(_ aNotification: Notification) { 10 | // Create the SwiftUI view that provides the window contents. 11 | let contentView = ContentView() 12 | 13 | // Create the window and set the content view. 14 | window = NSWindow( 15 | contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), 16 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], 17 | backing: .buffered, defer: false) 18 | window.isReleasedWhenClosed = false 19 | window.center() 20 | window.setFrameAutosaveName("Main Window") 21 | window.contentView = NSHostingView(rootView: contentView) 22 | window.makeKeyAndOrderFront(nil) 23 | 24 | Self.initializeFullScreenDetection() 25 | setMenuBar(to: MainMenuBar()) 26 | } 27 | 28 | func applicationWillTerminate(_ aNotification: Notification) { 29 | // Insert code here to tear down your application 30 | } 31 | 32 | fileprivate static var fullScreenDetectionNeedsInitialization = true 33 | fileprivate static var fullScreenLock = os_unfair_lock() 34 | internal fileprivate(set) static var isFullScreen: Bool = false 35 | 36 | // ------------------------------------- 37 | internal static func initializeFullScreenDetection() 38 | { 39 | guard fullScreenDetectionNeedsInitialization else { return } 40 | defer { fullScreenDetectionNeedsInitialization = false } 41 | 42 | _ = NotificationCenter.default.addObserver( 43 | forName: NSWindow.willEnterFullScreenNotification, 44 | object: nil, 45 | queue: nil) 46 | { _ in 47 | fullScreenLock.withLock { isFullScreen = true } 48 | } 49 | _ = NotificationCenter.default.addObserver( 50 | forName: NSWindow.willExitFullScreenNotification, 51 | object: nil, 52 | queue: nil) 53 | { _ in 54 | fullScreenLock.withLock { isFullScreen = false } 55 | } 56 | } 57 | } 58 | 59 | // ------------------------------------- 60 | fileprivate extension os_unfair_lock 61 | { 62 | mutating func withLock(block: () throws -> R) rethrows -> R 63 | { 64 | os_unfair_lock_lock(&self) 65 | defer { os_unfair_lock_unlock(&self) } 66 | 67 | return try block() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/ThemeColorWell.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | // 21 | 22 | import SwiftUI 23 | 24 | // ------------------------------------- 25 | struct ThemeColorWell: View 26 | { 27 | let label: String 28 | @Binding var currentTheme: Theme 29 | let colorPath: WritableKeyPath 30 | 31 | init( 32 | _ label: String, 33 | currentTheme: Binding, 34 | colorPath: WritableKeyPath) 35 | { 36 | self.label = label 37 | self._currentTheme = currentTheme 38 | self.colorPath = colorPath 39 | } 40 | 41 | // ------------------------------------- 42 | public var body: some View 43 | { 44 | HStack(spacing: 0) 45 | { 46 | Text("\(label):") 47 | .font(.system(size: 10)) 48 | .frame(alignment: .trailing) 49 | .padding([.leading, .trailing], 5) 50 | ColorWell($currentTheme, colorPath: colorPath) 51 | .frame(width: 50, height: 20, alignment: .leading) 52 | } 53 | } 54 | } 55 | 56 | // ------------------------------------- 57 | struct ThemeColorWell_Previews: PreviewProvider 58 | { 59 | static let prefs = Preferences() 60 | @State static var currentTheme = prefs.theme 61 | 62 | // ------------------------------------- 63 | static var previews: some View { 64 | ThemeColorWell("Some Color", currentTheme: $currentTheme, colorPath: \.validGuessColor) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/Actions/SelectorAction.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | 23 | // ------------------------------------- 24 | public struct SelectorAction: Action 25 | { 26 | public var selector: Selector? = nil 27 | public var keyEquivalent: KeyEquivalent? = nil 28 | public var enabledValidator: (() -> Bool)? = nil 29 | 30 | // ------------------------------------- 31 | @inlinable 32 | public init(keyEquivalent: KeyEquivalent?, selector: Selector?) 33 | { 34 | self.selector = selector 35 | self.keyEquivalent = keyEquivalent 36 | } 37 | 38 | // ------------------------------------- 39 | public var canBeEnabled: Bool = true 40 | 41 | // ------------------------------------- 42 | public var isEnabled: Bool 43 | { 44 | guard canBeEnabled && selector != nil else { return false } 45 | 46 | if let validator = enabledValidator { 47 | return validator() 48 | } 49 | 50 | let responderChain = ResponderChain(forContextualMenu: false) 51 | return nil != responderChain.firstResponder { 52 | $0.responds(to: selector) 53 | } 54 | } 55 | 56 | // ------------------------------------- 57 | public func performActionSelf( 58 | on target: ActionResponder?, 59 | for sender: Any?) -> Bool 60 | { 61 | guard canBeEnabled, let nsTarget = target as? NSObject else { 62 | return false 63 | } 64 | 65 | nsTarget.performAction(self, for: sender) 66 | return true 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Swift Type Extensions/StringProtocol+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import AppKit 22 | 23 | // ------------------------------------- 24 | extension StringProtocol 25 | { 26 | // ------------------------------------- 27 | func attributedString(_ font: NSFont) -> NSAttributedString { 28 | attributedString([.font: font]) 29 | } 30 | 31 | // ------------------------------------- 32 | func attributedString( 33 | _ attributes: [NSAttributedString.Key: Any]) -> NSAttributedString 34 | { 35 | NSAttributedString(string: String(self), attributes: attributes) 36 | } 37 | 38 | // ------------------------------------- 39 | func lastRange(of substr: S) -> Range? 40 | { 41 | var s = self[...] 42 | var foundRange: Range? = nil 43 | while let r = s.range(of: substr) 44 | { 45 | foundRange = r 46 | s = s[r.upperBound...] 47 | } 48 | 49 | return foundRange 50 | } 51 | 52 | // ------------------------------------- 53 | var rangeOfNumericSuffix: Range? 54 | { 55 | guard count > 0 && last!.isNumber else { return nil } 56 | 57 | guard let lastNonDigitIndex = lastIndex(where: { !$0.isNumber }) else { 58 | return startIndex.. 13 | 14 | var body: some View { 15 | List { 16 | ForEach(items) { item in 17 | Text("Item at \(item.timestamp!, formatter: itemFormatter)") 18 | } 19 | .onDelete(perform: deleteItems) 20 | } 21 | .toolbar { 22 | Button(action: addItem) { 23 | Label("Add Item", systemImage: "plus") 24 | } 25 | } 26 | } 27 | 28 | private func addItem() { 29 | withAnimation { 30 | let newItem = Item(context: viewContext) 31 | newItem.timestamp = Date() 32 | 33 | do { 34 | try viewContext.save() 35 | } catch { 36 | // Replace this implementation with code to handle the error appropriately. 37 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 38 | let nsError = error as NSError 39 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 40 | } 41 | } 42 | } 43 | 44 | private func deleteItems(offsets: IndexSet) { 45 | withAnimation { 46 | offsets.map { items[$0] }.forEach(viewContext.delete) 47 | 48 | do { 49 | try viewContext.save() 50 | } catch { 51 | // Replace this implementation with code to handle the error appropriately. 52 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 53 | let nsError = error as NSError 54 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 55 | } 56 | } 57 | } 58 | } 59 | 60 | private let itemFormatter: DateFormatter = { 61 | let formatter = DateFormatter() 62 | formatter.dateStyle = .short 63 | formatter.timeStyle = .medium 64 | return formatter 65 | }() 66 | 67 | struct ContentView_Previews: PreviewProvider { 68 | static var previews: some View { 69 | ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/CellGroupView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | struct CellGroupView: View 25 | { 26 | @EnvironmentObject var puzzle: PuzzleObject 27 | @EnvironmentObject var prefs: Preferences 28 | 29 | static let cellSpacing: CGFloat = 1 30 | static let width = CellView.width * 3 + cellSpacing * 2 31 | static let height = width 32 | static let size = CGSize(width: width, height: height) 33 | 34 | let row: Int 35 | let col: Int 36 | 37 | // ------------------------------------- 38 | var body: some View 39 | { 40 | VStack(spacing: Self.cellSpacing) 41 | { 42 | ForEach(0..<3) 43 | { i in 44 | HStack(spacing: Self.cellSpacing) 45 | { 46 | ForEach(0..<3) 47 | { j in 48 | CellView( 49 | row: row * 3 + i, 50 | col: col * 3 + j 51 | ) 52 | .environmentObject(puzzle) 53 | .environmentObject(prefs) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | // ------------------------------------- 62 | struct CellGroupView_Previews: PreviewProvider 63 | { 64 | @State static var puzzle = previewPuzzle 65 | 66 | // ------------------------------------- 67 | static var previews: some View 68 | { 69 | CellGroupView(row: 0, col: 0 ) 70 | .environmentObject(previewPuzzle) 71 | .environmentObject(Preferences()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/ThemeFontPicker.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | // 21 | 22 | import SwiftUI 23 | 24 | // ------------------------------------- 25 | struct ThemeFontPicker: View 26 | { 27 | static let sizes = [8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 28, 30] 28 | var label: String 29 | @State var pickerValue: NSFont 30 | @State var size: Int 31 | @Binding var currentTheme: Theme 32 | let keyPath: WritableKeyPath 33 | 34 | // ------------------------------------- 35 | var body: some View 36 | { 37 | HStack(alignment: .center, spacing: 0) 38 | { 39 | Text("\(label):") 40 | .font(.system(size: 10)) 41 | .foregroundColor(.controlTextColor) 42 | .frame(alignment: .trailing) 43 | .padding(.trailing, 5) 44 | 45 | FontFamilyPopupButton( 46 | width: 80, 47 | height: 25, 48 | valuePath: keyPath, 49 | in: $currentTheme 50 | ).frame(alignment: .trailing) 51 | 52 | Text("Size:") 53 | .font(.system(size: 10)) 54 | .foregroundColor(.controlTextColor) 55 | .frame(alignment: .trailing) 56 | .padding([.leading, .trailing], 5) 57 | 58 | FontSizePopupButton( 59 | width: 45, 60 | height: 25, 61 | valuePath: keyPath, 62 | in: $currentTheme, 63 | content: { Self.sizes } 64 | ) 65 | .frame(width: 45, height: 25) 66 | }.frame(width: 200) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Menu Bar/FileMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | import MacMenuBar 23 | 24 | // ------------------------------------- 25 | let fileMenu = StandardMenu(title: "File") 26 | { 27 | TextMenuItem(title: "New Game", keyEquivalent: .command + "n") { _ in 28 | AppDelegate.shared.newGame() 29 | } 30 | .enabledWhen { SheetRequest.shared.state == .none } 31 | 32 | TextMenuItem(title: "Open...", keyEquivalent: .command + "o") { sender in 33 | AppDelegate.shared.openDocument(sender) 34 | } 35 | StandardMenu(title: "Open Recent...") 36 | { 37 | ForEach(Preferences.shared.recentBookmarks) 38 | { bookmark in 39 | TextMenuItem(title: bookmark.deletingPathExtension().lastPathComponent) 40 | { _ in 41 | PuzzleObject.shared.load(from: bookmark) 42 | } 43 | } 44 | } 45 | 46 | MenuSeparator() 47 | 48 | TextMenuItem(title: "Save", keyEquivalent: .command + "s") { sender in 49 | AppDelegate.shared.save(sender) 50 | }.updatingTitleWith { 51 | PuzzleObject.shared.bookmark == nil ? "Save..." : "Save" 52 | } 53 | 54 | TextMenuItem(title: "Save As...") { sender in 55 | AppDelegate.shared.saveTo(sender) 56 | } 57 | TextMenuItem(title: "Revert to Saved", keyEquivalent: .command + "r") 58 | { sender in 59 | PuzzleObject.shared.load(from: PuzzleObject.shared.bookmark!) 60 | }.enabledWhen { 61 | PuzzleObject.shared.bookmark != nil 62 | } 63 | 64 | MenuSeparator() 65 | 66 | TextMenuItem(title: "Page Setup...", action: .pageSetup).enabled(false) 67 | TextMenuItem(title: "Print", action: .print).enabled(false) 68 | } 69 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/SwiftUI Type Extensions/Color+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | extension Color 25 | { 26 | static var controlColor: Color { Color(.controlColor) } 27 | static var controlBackgroundColor: Color { Color(.controlBackgroundColor) } 28 | static var controlAccentColor: Color { Color(.controlAccentColor) } 29 | static var controlTextColor: Color { Color(.controlTextColor) } 30 | static var selectedControlTextColor: Color { 31 | Color(.selectedControlTextColor) 32 | } 33 | static var disabledControlTextColor: Color { 34 | Color(.disabledControlTextColor) 35 | } 36 | static var controlShadowColor: Color { Color(.controlShadowColor) } 37 | static var controlHighlightColor: Color { Color(.controlHighlightColor) } 38 | static var controlDarkShadowColor: Color { Color(.controlDarkShadowColor) } 39 | static var controlLightHighlightColor: Color { 40 | Color(.controlLightHighlightColor) 41 | } 42 | static var selectedControlColor: Color { Color(.selectedControlColor) } 43 | static var secondarySelectedControlColor: Color { 44 | Color(.secondarySelectedControlColor) 45 | } 46 | static var alternateSelectedControlColor: Color { 47 | Color(.alternateSelectedControlColor) 48 | } 49 | static var alternateSelectedControlTextColor: Color { 50 | Color(.alternateSelectedControlTextColor) 51 | } 52 | 53 | static var windowFrameColor: Color { Color(.windowFrameColor) } 54 | static var windowBackgroundColor: Color { Color(.windowBackgroundColor) } 55 | static var windowFrameTextColor: Color { Color(.windowFrameTextColor) } 56 | 57 | static var gridColor: Color { Color(.gridColor) } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/AppKit Subclasses/NSMenu+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public extension NSMenu 25 | { 26 | // ------------------------------------- 27 | func firstMenuItem(where condition: (NSMenuItem) -> Bool) -> NSMenuItem? 28 | { 29 | for item in items 30 | { 31 | if let submenu = item.submenu 32 | { 33 | if let foundItem = submenu.firstMenuItem(where: condition) { 34 | return foundItem 35 | } 36 | } 37 | else if condition(item) { return item } 38 | } 39 | 40 | return nil 41 | } 42 | 43 | // ------------------------------------- 44 | func lastMenuItem(where condition: (NSMenuItem) -> Bool) -> NSMenuItem? 45 | { 46 | for item in items.reversed() 47 | { 48 | if let submenu = item.submenu 49 | { 50 | if let foundItem = submenu.lastMenuItem(where: condition) { 51 | return foundItem 52 | } 53 | } 54 | else if condition(item) { return item } 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // ------------------------------------- 61 | internal func selectorAlreadyAdded(_ selector: Selector?) -> Bool 62 | { 63 | guard let selector = selector else { return false } 64 | return nil != rootMenu.firstMenuItem { $0.action == selector } 65 | } 66 | 67 | // ------------------------------------- 68 | internal var rootMenu: NSMenu { 69 | var curMenu = self 70 | while curMenu.supermenu != nil { 71 | curMenu = curMenu.supermenu! 72 | } 73 | return curMenu 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/CellNotesView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | struct CellNotesView: View 25 | { 26 | @EnvironmentObject var puzzle: PuzzleObject 27 | @EnvironmentObject var prefs: Preferences 28 | let row: Int 29 | let col: Int 30 | 31 | // ------------------------------------- 32 | func note(_ noteRow: Int, _ noteCol: Int) -> Int? 33 | { 34 | let noteValue = 3 * noteRow + noteCol + 1 35 | return puzzle[row, col].notes.contains(noteValue) ? noteValue : nil 36 | } 37 | 38 | // ------------------------------------- 39 | var body: some View 40 | { 41 | VStack(spacing:0) 42 | { 43 | ForEach(0..<3) 44 | { noteRow in 45 | HStack(spacing: 0) 46 | { 47 | ForEach(0..<3) 48 | { noteCol in 49 | CellNoteView(note: note(noteRow, noteCol)) 50 | .environmentObject(prefs) 51 | } 52 | } 53 | .scaledToFit() 54 | } 55 | } 56 | .scaledToFit() 57 | } 58 | } 59 | 60 | // ------------------------------------- 61 | internal let previewPuzzle: PuzzleObject = 62 | { 63 | let p = PuzzleObject() 64 | for i in p.puzzle.cells.indices { 65 | p.puzzle.cells[i].notes = Set(1...9) 66 | } 67 | let i = p.puzzle.cells.firstIndex { !$0.fixed && $0.guess == nil }! 68 | p.selection = (i / 9, i % 9) 69 | return p 70 | }() 71 | 72 | // ------------------------------------- 73 | struct CellNotesView_Previews: PreviewProvider 74 | { 75 | static var puzzle = previewPuzzle 76 | 77 | static var previews: some View 78 | { 79 | CellNotesView(row: 0, col: 0) 80 | .environmentObject(puzzle) 81 | .environmentObject(Preferences()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenus/MenuItemGroup.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | 23 | // ------------------------------------- 24 | public struct MenuItemGroup: MenuElement 25 | { 26 | public internal(set) var nsMenu: NSMacMenu 27 | 28 | // ------------------------------------- 29 | public init(menu: StandardMenu?) { 30 | self.init(menu?.nsMenu ?? NSMacMenu()) 31 | } 32 | 33 | // ------------------------------------- 34 | public init(title: String) { 35 | self.init(NSMacMenu(title: title)) 36 | } 37 | 38 | // ------------------------------------- 39 | public init(title: String, @MenuBuilder items: () -> [MenuElement]) 40 | { 41 | self.init(items: items) 42 | self.nsMenu.title = title 43 | } 44 | 45 | // ------------------------------------- 46 | public init(_ nsMenu: NSMacMenu) { 47 | self.nsMenu = nsMenu 48 | nsMenu.delegate = nsMenu 49 | } 50 | } 51 | 52 | // ------------------------------------- 53 | extension MenuItemGroup: MacMenu 54 | { 55 | public init() { 56 | self.init(menu: nil) 57 | } 58 | 59 | // ------------------------------------- 60 | @inlinable public var isItem: Bool { false } 61 | 62 | // ------------------------------------- 63 | @inlinable public var title: String 64 | { 65 | get { nsMenu.title } 66 | set { nsMenu.title = newValue } 67 | } 68 | 69 | // ------------------------------------- 70 | @inlinable public var isVisible: Bool 71 | { 72 | get { !(nsMenu.nsMacMenuItem?.isHidden ?? true) } 73 | set { nsMenu.nsMacMenuItem?.isHidden = !newValue } 74 | } 75 | 76 | // ------------------------------------- 77 | @inlinable public var canBeEnabled: Bool 78 | { 79 | get { false } 80 | set { } 81 | } 82 | 83 | // ------------------------------------- 84 | @inlinable public var isEnabled: Bool { false } 85 | } 86 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Cocoa-BasedTypes/NSGameHostingView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | import Cocoa 23 | 24 | // ------------------------------------- 25 | fileprivate extension NSEvent 26 | { 27 | // ------------------------------------- 28 | var translateRightMouseButtonEvent: NSEvent 29 | { 30 | guard let cgEvent = self.cgEvent else { return self } 31 | 32 | switch type 33 | { 34 | case .rightMouseDown: cgEvent.type = .leftMouseDown 35 | case .rightMouseUp: cgEvent.type = .leftMouseUp 36 | case .rightMouseDragged: cgEvent.type = .leftMouseDragged 37 | 38 | default: return self 39 | } 40 | 41 | cgEvent.flags.formUnion(.maskControl) 42 | 43 | guard let nsEvent = NSEvent(cgEvent: cgEvent) else { return self } 44 | 45 | return nsEvent 46 | } 47 | } 48 | 49 | // ------------------------------------- 50 | /* 51 | Translates right-mouse-button events to left-mouse-button events with a 52 | .control modifier. 53 | */ 54 | class NSGameHostingView: NSHostingView 55 | { 56 | // ------------------------------------- 57 | @objc public override func rightMouseDown(with event: NSEvent) { 58 | super.mouseDown(with: event.translateRightMouseButtonEvent) 59 | } 60 | 61 | // ------------------------------------- 62 | @objc public override func rightMouseUp(with event: NSEvent) { 63 | super.mouseUp(with: event.translateRightMouseButtonEvent) 64 | } 65 | 66 | // ------------------------------------- 67 | @objc public override func rightMouseDragged(with event: NSEvent) { 68 | super.mouseDragged(with: event.translateRightMouseButtonEvent) 69 | } 70 | 71 | // ------------------------------------- 72 | @objc public func keyDown(with event: NSEvent) -> Bool { 73 | return KeyResponder.current?.closure?(event) ?? false 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenus/StandardMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | // ------------------------------------- 22 | public struct StandardMenu 23 | { 24 | public internal(set) var nsMenu: NSMacMenu 25 | 26 | // ------------------------------------- 27 | public init(menu: StandardMenu?) { 28 | self.init(menu?.nsMenu ?? NSMacMenu()) 29 | } 30 | 31 | // ------------------------------------- 32 | public init(title: String) { 33 | self.init(NSMacMenu(title: title)) 34 | } 35 | 36 | // ------------------------------------- 37 | public init(title: String, @MenuBuilder items: () -> [MenuElement]) 38 | { 39 | self.init(items: items) 40 | self.nsMenu.title = title 41 | self.isVisible = true 42 | self.canBeEnabled = true 43 | } 44 | 45 | // ------------------------------------- 46 | public init(_ nsMenu: NSMacMenu) { 47 | self.nsMenu = nsMenu 48 | nsMenu.delegate = nsMenu 49 | } 50 | } 51 | 52 | // ------------------------------------- 53 | extension StandardMenu: MacMenu 54 | { 55 | public init() { 56 | self.init(menu: nil) 57 | } 58 | 59 | // ------------------------------------- 60 | @inlinable public var isItem: Bool { false } 61 | 62 | // ------------------------------------- 63 | @inlinable public var title: String 64 | { 65 | get { nsMenu.title } 66 | set { nsMenu.title = newValue } 67 | } 68 | 69 | // ------------------------------------- 70 | @inlinable public var isVisible: Bool 71 | { 72 | get { !(nsMenu.nsMacMenuItem?.isHidden ?? true) } 73 | set { nsMenu.nsMacMenuItem?.isHidden = !newValue } 74 | } 75 | 76 | // ------------------------------------- 77 | @inlinable public var canBeEnabled: Bool 78 | { 79 | get { nsMenu.nsMacMenuItem?.isEnabled ?? false } 80 | set { nsMenu.nsMacMenuItem?.canBeEnabled = newValue } 81 | } 82 | 83 | // ------------------------------------- 84 | @inlinable public var isEnabled: Bool { 85 | nsMenu.nsMacMenuItem?.isEnabled ?? false 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenus/MacMenu.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public protocol MacMenu: MenuElement 25 | { 26 | var nsMenu: NSMacMenu { get } 27 | 28 | mutating func append(item: T) 29 | mutating func append(submenu: T) 30 | 31 | init() 32 | } 33 | 34 | // ------------------------------------- 35 | extension MacMenu 36 | { 37 | // ------------------------------------- 38 | init(@MenuBuilder items: () -> [MenuElement]) 39 | { 40 | self.init() 41 | items().forEach { 42 | $0.appendSelf(to: &self) 43 | } 44 | } 45 | 46 | // ------------------------------------- 47 | init(items: [MacMenu]) 48 | { 49 | self.init() 50 | items.forEach { 51 | $0.appendSelf(to: &self) 52 | } 53 | } 54 | 55 | // ------------------------------------- 56 | @inlinable 57 | public func refuseAutoinjectedMenuItems(_ shouldRefuse: Bool = true) -> Self 58 | { 59 | nsMenu.refuseAutoinjectedItems = shouldRefuse 60 | return self 61 | } 62 | 63 | // ------------------------------------- 64 | @inlinable 65 | public func appendSelf(to menu: inout T) { 66 | menu.append(submenu: self) 67 | } 68 | 69 | // ------------------------------------- 70 | @inlinable 71 | public mutating func append(item: T) { 72 | nsMenu.addItem(item.nsMenuItem) 73 | } 74 | 75 | // ------------------------------------- 76 | @inlinable 77 | public mutating func append(submenu: T) 78 | { 79 | let nsMenu = self.nsMenu 80 | 81 | if let group = submenu as? MenuItemGroup 82 | { 83 | if let lastItem = nsMenu.items.last, !lastItem.isSeparatorItem { 84 | nsMenu.addItem(NSMenuItem.separator()) 85 | } 86 | 87 | for item in group.nsMenu.items { 88 | nsMenu.addItem(item) 89 | } 90 | } 91 | else { 92 | nsMenu.addSubmenu(submenu.nsMenu) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Foundation Extensions/CGPoint+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Foundation 22 | 23 | // ------------------------------------- 24 | public extension CGPoint 25 | { 26 | // ------------------------------------- 27 | static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 28 | CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 29 | } 30 | 31 | // ------------------------------------- 32 | static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 33 | CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) 34 | 35 | } 36 | 37 | // ------------------------------------- 38 | static func + (lhs: CGPoint, rhs: CGSize) -> CGPoint { 39 | CGPoint(x: lhs.x + rhs.width, y: lhs.y + rhs.height) 40 | } 41 | 42 | // ------------------------------------- 43 | static func - (lhs: CGPoint, rhs: CGSize) -> CGPoint { 44 | CGPoint(x: lhs.x - rhs.width, y: lhs.y - rhs.height) 45 | } 46 | 47 | // ------------------------------------- 48 | static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint { 49 | CGPoint(x: lhs.x * rhs, y: lhs.y * rhs) 50 | } 51 | 52 | // ------------------------------------- 53 | static func * (lhs: CGFloat, rhs: CGPoint) -> CGPoint { 54 | return rhs * lhs 55 | } 56 | 57 | // ------------------------------------- 58 | static func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint { 59 | CGPoint(x: lhs.x / rhs, y: lhs.y / rhs) 60 | } 61 | 62 | // ------------------------------------- 63 | func midPoint(to other: CGPoint) -> CGPoint { 64 | return (self + other) / 2 65 | } 66 | 67 | // ------------------------------------- 68 | func translate(by delta: CGSize) -> CGPoint { 69 | return self + CGPoint(x: delta.width, y: delta.height) 70 | } 71 | 72 | // ------------------------------------- 73 | func translate(deltaX: CGFloat, deltaY: CGFloat = 0) -> CGPoint { 74 | self + CGSize(width: deltaX, height: deltaY) 75 | } 76 | 77 | // ------------------------------------- 78 | func translate(deltaY: CGFloat) -> CGPoint { 79 | self + CGSize(width: 0, height: deltaY) 80 | } 81 | 82 | // ------------------------------------- 83 | var vectorLength: CGFloat { sqrt(x * x + y * y) } 84 | } 85 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/Keyboard Hacks/KeyResponder.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | fileprivate extension os_unfair_lock 25 | { 26 | mutating func withLock(do block: () throws -> R) rethrows -> R 27 | { 28 | os_unfair_lock_lock(&self) 29 | defer { os_unfair_lock_unlock(&self) } 30 | return try block() 31 | } 32 | } 33 | 34 | // ------------------------------------- 35 | final class KeyResponder 36 | { 37 | private static var respondersLock = os_unfair_lock() 38 | private static var keyResponders = [KeyResponder]() 39 | static var current: KeyResponder? { 40 | respondersLock.withLock { keyResponders.last } 41 | } 42 | 43 | private static var nextID = 0 44 | 45 | let id: Int = { nextID += 1; return nextID }() 46 | public private(set) var isResponding: Bool = false 47 | var closure: ((NSEvent) -> Bool)? = nil 48 | 49 | // ------------------------------------- 50 | func onKeyDown(do body: @escaping ((NSEvent) -> Bool)) 51 | { 52 | Self.respondersLock.withLock 53 | { 54 | // Nest closures so multiple closures can have a crack at the event 55 | let existingClosure = self.closure 56 | self.closure = 57 | { 58 | if !(existingClosure?($0) ?? false) { 59 | return body($0) 60 | } 61 | return false 62 | } 63 | 64 | if let i = Self.keyResponders.lastIndex(where: { $0.id == self.id }) 65 | { 66 | Self.keyResponders.remove(at: i) 67 | } 68 | Self.keyResponders.append(self) 69 | } 70 | isResponding = true 71 | } 72 | 73 | // ------------------------------------- 74 | func defaultHandler() { 75 | onKeyDown {_ in false } 76 | } 77 | 78 | // ------------------------------------- 79 | func resign() 80 | { 81 | Self.respondersLock.withLock 82 | { 83 | closure = nil 84 | if let i = Self.keyResponders.lastIndex(where: { $0.id == self.id }) 85 | { 86 | Self.keyResponders.remove(at: i) 87 | } 88 | } 89 | isResponding = false 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | class SheetRequest: ObservableObject 25 | { 26 | static var shared: SheetRequest { AppDelegate.shared.sheetRequest } 27 | 28 | enum Request 29 | { 30 | case none 31 | case newGame 32 | case editThemes 33 | } 34 | 35 | @Published var state: Request = .none 36 | } 37 | 38 | // ------------------------------------- 39 | struct ContentView: View 40 | { 41 | @State var puzzle = PuzzleObject() 42 | @ObservedObject var sheetRequest: SheetRequest 43 | @State var startNewGame = false 44 | @ObservedObject var prefs: Preferences 45 | 46 | static let borderWidth: CGFloat = 3 47 | static let width = PuzzleView.width + 2 * borderWidth 48 | static let height = width 49 | 50 | // ------------------------------------- 51 | var requestedSheet: some View 52 | { 53 | Group 54 | { 55 | if sheetRequest.state == .newGame 56 | { 57 | NewGameRequestSheet() 58 | .zIndex(2) 59 | .environmentObject(puzzle) 60 | .environmentObject(sheetRequest) 61 | .environmentObject(prefs) 62 | } 63 | else if sheetRequest.state == .editThemes 64 | { 65 | ThemeEditor() 66 | .environmentObject(sheetRequest) 67 | .environmentObject(prefs) 68 | } 69 | else { EmptyView() } 70 | } 71 | } 72 | 73 | // ------------------------------------- 74 | var body: some View 75 | { 76 | ZStack 77 | { 78 | Color(prefs.theme.borderColor) 79 | PuzzleView() 80 | .environmentObject(puzzle) 81 | .environmentObject(sheetRequest) 82 | .environmentObject(prefs) 83 | 84 | requestedSheet 85 | } 86 | .frame(width: Self.width, height: Self.height, alignment: .center) 87 | } 88 | } 89 | 90 | // ------------------------------------- 91 | struct ContentView_Previews: PreviewProvider { 92 | static let prefs = Preferences() 93 | static var previews: some View { 94 | ContentView(sheetRequest: SheetRequest(), prefs: prefs) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/NSViewRepresentables/FontSizePopupButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontSizePopupButton.swift 3 | // Sudoku 4 | // 5 | // Created by Chip Jarred on 3/29/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // ------------------------------------- 11 | struct FontSizePopupButton: PopupButtonProtocol 12 | { 13 | typealias Value = NSFont 14 | typealias ValueContainer = ValueContainer 15 | 16 | static var fontSize: CGFloat { 17 | FontFamilyPopupButton.fontSize 18 | } 19 | 20 | var width: CGFloat 21 | var height: CGFloat 22 | var valueContainer: Binding 23 | var valuePath: ValuePath 24 | var content: () -> [NSFont] 25 | 26 | // ------------------------------------- 27 | init( 28 | width: CGFloat, 29 | height: CGFloat, 30 | valuePath: ValuePath, 31 | in container: Binding, 32 | content: @escaping () -> [NSFont]) 33 | { 34 | self.width = width 35 | self.height = height 36 | self.valueContainer = container 37 | self.valuePath = valuePath 38 | self.content = content 39 | } 40 | 41 | // ------------------------------------- 42 | init( 43 | width: CGFloat, 44 | height: CGFloat, 45 | valuePath: ValuePath, 46 | in container: Binding, 47 | content: @escaping () -> [Int]) 48 | { 49 | let newContent: () -> [NSFont] = 50 | { 51 | content().map 52 | { 53 | guard let familyName = 54 | container.wrappedValue[keyPath: valuePath].familyName 55 | else { return nil } 56 | 57 | return NSFontManager.shared.font( 58 | withFamily: familyName, 59 | traits: [], 60 | weight: 5, 61 | size: CGFloat($0) 62 | ) 63 | }.filter { $0 != nil }.map { $0! } 64 | } 65 | 66 | self.init( 67 | width: width, 68 | height: height, 69 | valuePath: valuePath, 70 | in: container, 71 | content: newContent 72 | ) 73 | } 74 | 75 | // ------------------------------------- 76 | func itemsAreEqual(_ value1: Value, _ value2: Value) -> Bool { 77 | return value1.pointSize == value2.pointSize 78 | } 79 | 80 | // ------------------------------------- 81 | func attributedItemTitle(from value: Value) -> NSAttributedString? 82 | { 83 | return NSAttributedString( 84 | string: Int(value.pointSize).description, 85 | attributes: 86 | [.font : NSFont.systemFont(ofSize: Self.fontSize) as Any] 87 | ) 88 | } 89 | 90 | // ------------------------------------- 91 | func itemTitle(from value: Value) -> String? { nil } 92 | 93 | // ------------------------------------- 94 | func value(for itemTitle: String) -> Value? 95 | { 96 | guard let familyName = currentValue.familyName, 97 | let fontSize = Int(itemTitle) 98 | else { return nil } 99 | 100 | return NSFontManager.shared.font( 101 | withFamily: familyName, 102 | traits: [], 103 | weight: 5, 104 | size: CGFloat(fontSize) 105 | ) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/OKCancelRequestSheet.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | import MacMenuBar 23 | 24 | // ------------------------------------- 25 | protocol OKCancelRequestSheet: View 26 | { 27 | var keyResponder: KeyResponder { get } 28 | var title: String { get } 29 | var description: String { get } 30 | var okText: String { get } 31 | var cancelText: String? { get } 32 | 33 | func ok() 34 | func cancel() 35 | } 36 | 37 | // ------------------------------------- 38 | extension OKCancelRequestSheet 39 | { 40 | // ------------------------------------- 41 | var body: some View 42 | { 43 | KeyResponderGroup 44 | { 45 | ZStack 46 | { 47 | Rectangle().fill(Color.controlBackgroundColor).opacity(0.8) 48 | VStack(spacing: 0) 49 | { 50 | Text(title).font(.title) 51 | .foregroundColor(.controlTextColor) 52 | .padding(.top, 20) 53 | .padding(.bottom, 50) 54 | 55 | Text(description) 56 | .foregroundColor(.controlTextColor) 57 | .padding([.leading, .trailing], 20) 58 | .padding(.bottom, 40) 59 | 60 | HStack 61 | { 62 | Spacer() 63 | 64 | if let cancelText = self.cancelText 65 | { 66 | MacOSButton( 67 | cancelText, 68 | keyEquivalent: .command + "." 69 | ) { cancel() } 70 | .frame(width: 100) 71 | } 72 | 73 | MacOSButton(okText, keyEquivalent: .none + "\r") { 74 | ok() 75 | }.focusable() 76 | .frame(width: 100) 77 | 78 | Spacer() 79 | } 80 | 81 | Spacer() 82 | } 83 | } 84 | } 85 | .transition( 86 | AnyTransition.opacity.animation( 87 | .easeInOut(duration: 0.3) 88 | ) 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/NSViewRepresentables/ColorWell.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | // ------------------------------------- 24 | struct ColorWell: NSViewRepresentable 25 | { 26 | typealias NSViewType = CustomNSColorWell 27 | typealias NSColorPath = WritableKeyPath 28 | 29 | @Binding var coloredThing: ColoredThing 30 | var colorPath: NSColorPath 31 | 32 | // ------------------------------------- 33 | init( 34 | _ coloredThing: Binding, 35 | colorPath: NSColorPath) 36 | { 37 | self._coloredThing = coloredThing 38 | self.colorPath = colorPath 39 | } 40 | 41 | // ------------------------------------- 42 | class CustomNSColorWell: NSColorWell 43 | { 44 | @Binding var coloredThing: ColoredThing 45 | var colorPath: NSColorPath 46 | var canInterceptUpdates = false 47 | 48 | // ------------------------------------- 49 | init( 50 | _ coloredThing: Binding, 51 | colorPath: NSColorPath) 52 | { 53 | self._coloredThing = coloredThing 54 | self.colorPath = colorPath 55 | super.init(frame: .zero) 56 | super.color = coloredThing.wrappedValue[keyPath: colorPath] 57 | self.canInterceptUpdates = true 58 | } 59 | 60 | // ------------------------------------- 61 | required init?(coder: NSCoder) { 62 | fatalError("init(coder:) has not been implemented") 63 | } 64 | 65 | // ------------------------------------- 66 | override var color: NSColor 67 | { 68 | didSet 69 | { 70 | if canInterceptUpdates { 71 | self.coloredThing[keyPath: colorPath] = super.color 72 | } 73 | } 74 | } 75 | } 76 | 77 | // ------------------------------------- 78 | func makeNSView(context: Context) -> NSViewType { 79 | return CustomNSColorWell($coloredThing, colorPath: colorPath) 80 | } 81 | 82 | // ------------------------------------- 83 | func updateNSView(_ nsView: NSViewType, context: Context) 84 | { 85 | DispatchQueue.main.async { 86 | nsView.color = coloredThing[keyPath: colorPath] 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/EditMenu.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MacMenuBar 3 | 4 | // ------------------------------------- 5 | let editMenu = StandardMenu(title: "Edit") 6 | { 7 | TextMenuItem(title: "Undo", action: .undo) 8 | TextMenuItem(title: "Redo", action: .redo) 9 | 10 | MenuSeparator() 11 | 12 | TextMenuItem(title: "Cut", action: .cut) 13 | TextMenuItem(title: "Copy", action: .copy) 14 | TextMenuItem(title: "Paste", action: .paste) 15 | TextMenuItem(title: "Paste and Match Style", action: .pasteAndMatchStyle) 16 | TextMenuItem(title: "Delete", action: .delete) 17 | TextMenuItem(title: "Select All", action: .selectAll) 18 | 19 | MenuSeparator() 20 | 21 | StandardMenu(title: "Find") 22 | { 23 | TextMenuItem(title: "Find...", action: .find) 24 | TextMenuItem(title: "Find and Replace...", action: .findAndReplace) 25 | TextMenuItem(title: "Find Next", action: .findNext) 26 | TextMenuItem(title: "Find Previous", action: .findPrevious) 27 | TextMenuItem( 28 | title: "Use Selection for Find", 29 | action: .useSelectionForFind 30 | ) 31 | TextMenuItem(title: "Jump to Selection", action: .jumpToSelection) 32 | } 33 | StandardMenu(title: "Spelling and Grammar") 34 | { 35 | TextMenuItem( 36 | title: "Show Spelling and Grammar", 37 | action: .showSpellingAndGrammar 38 | ) 39 | TextMenuItem(title: "Check Document Now", action: .checkDocumentNow) 40 | 41 | MenuSeparator() 42 | 43 | TextMenuItem( 44 | title: "Check Spelling While Typing", 45 | action: .checkSpellingWhileTyping 46 | ).toggleStateWhenSelected() 47 | TextMenuItem( 48 | title: "Check Grammar While Typing", 49 | action: .checkGrammarWhileTyping 50 | ).toggleStateWhenSelected() 51 | TextMenuItem( 52 | title: "Correct Spelling Automatically", 53 | action: .correctSpellingAutomatically 54 | ).toggleStateWhenSelected() 55 | } 56 | StandardMenu(title: "Substitutions") 57 | { 58 | TextMenuItem( 59 | title: "Show Substitutions", 60 | action: .showSpellingAndGrammar 61 | ) 62 | MenuSeparator() 63 | 64 | TextMenuItem(title: "Smart Copy/Paste", action: .smartCopyPaste) 65 | .toggleStateWhenSelected() 66 | TextMenuItem(title: "Smart Quotes", action: .smartQuotes) 67 | .toggleStateWhenSelected() 68 | TextMenuItem(title: "Smart Dashes", action: .smartDashes) 69 | .toggleStateWhenSelected() 70 | TextMenuItem(title: "Smart Links", action: .smartLinks) 71 | .toggleStateWhenSelected() 72 | TextMenuItem(title: "Data Detectors", action: .dataDetectors) 73 | .toggleStateWhenSelected() 74 | TextMenuItem(title: "Text Replacement", action: .textReplacement) 75 | .toggleStateWhenSelected() 76 | TextMenuItem(title: "Text Completion", action: .textCompletion) 77 | .toggleStateWhenSelected() 78 | } 79 | StandardMenu(title: "Transformation") 80 | { 81 | TextMenuItem(title: "Make Upper Case", action: .makeUppercase) 82 | TextMenuItem(title: "Make Lower Case", action: .makeLowercase) 83 | TextMenuItem(title: "Capitalize", action: .capitalize) 84 | } 85 | StandardMenu(title: "Speech") 86 | { 87 | TextMenuItem(title: "Start Speaking", action: .startSpeaking) 88 | TextMenuItem(title: "Stop Speaking", action: .stopSpeaking) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/ThemeEditorCellPreview.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | // 21 | 22 | import SwiftUI 23 | 24 | // ------------------------------------- 25 | struct ThemeEditorCellPreview: View 26 | { 27 | static let width = CellView.width 28 | static let height = CellView.height 29 | static let frame = CellView.frame 30 | static let gradient = CellView.gradient 31 | 32 | typealias HighlightPath = CellView.HighlightPath 33 | 34 | 35 | @Binding var currentTheme: Theme 36 | 37 | var isSelected: Bool 38 | var cell: Puzzle.Cell 39 | 40 | var fixed: Bool { cell.fixed } 41 | var value: Int { cell.value } 42 | var guess: Int? { cell.guess } 43 | var notes: Set { cell.notes } 44 | 45 | // ------------------------------------- 46 | var highlight: some View { 47 | CellView.highlightRect.opacity(currentTheme.highlightBrightness) 48 | } 49 | 50 | // ------------------------------------- 51 | var displayString: String 52 | { 53 | if cell.fixed { return "\(cell.value)" } 54 | if let guess = self.guess { 55 | return "\(guess)" 56 | } 57 | return "" 58 | } 59 | 60 | // ------------------------------------- 61 | var foreColor: Color 62 | { 63 | return Color( fixed 64 | ? currentTheme.valueColor 65 | : guess == value 66 | ? currentTheme.correctGuessColor 67 | : currentTheme.incorrectGuessColor 68 | ) 69 | } 70 | 71 | // ------------------------------------- 72 | var backColor: Color 73 | { 74 | return Color(fixed || guess == nil || guess == value 75 | ? currentTheme.backColor 76 | : currentTheme.incorrectBackColor 77 | ) 78 | } 79 | 80 | // ------------------------------------- 81 | var body: some View 82 | { 83 | ZStack 84 | { 85 | backColor 86 | if !fixed && isSelected { highlight } 87 | if !fixed && guess == nil 88 | { 89 | ThemeEditorCellNotesPreview( 90 | notes: cell.notes, 91 | currentTheme: $currentTheme 92 | ) 93 | } 94 | else 95 | { 96 | Text(displayString) 97 | .font(Font(currentTheme.font)) 98 | .foregroundColor(foreColor) 99 | } 100 | } 101 | .frame(width: Self.width, height: Self.height, alignment: .center) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/MacMenuBarTests/KeyEquivalent_StringParsing_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyEquivalent_StringParsing_Tests.swift 3 | // 4 | // 5 | // Created by Chip Jarred on 2/21/21. 6 | // 7 | 8 | import XCTest 9 | @testable import MacMenuBar 10 | 11 | // ------------------------------------- 12 | class KeyEquivalent_StringParsing_Tests: XCTestCase 13 | { 14 | // ------------------------------------- 15 | func test_can_parse_single_character() 16 | { 17 | var actual = KeyEquivalent.parse("c") 18 | XCTAssertEqual(actual!, .command + "c") 19 | 20 | actual = KeyEquivalent.parse("-") 21 | XCTAssertEqual(actual!, .command + "-") 22 | 23 | actual = KeyEquivalent.parse("\\") 24 | XCTAssertEqual(actual!, .command + "\\") 25 | } 26 | 27 | // ------------------------------------- 28 | func test_can_parse_singly_modified_character() 29 | { 30 | var actual = KeyEquivalent.parse("command-c") 31 | XCTAssertEqual(actual, .command + "c") 32 | 33 | actual = KeyEquivalent.parse("option-c") 34 | XCTAssertEqual(actual, .option + "c") 35 | 36 | actual = KeyEquivalent.parse("control-c") 37 | XCTAssertEqual(actual, .control + "c") 38 | 39 | // Unmofidifed keys or whose only mod is .shift add .command 40 | actual = KeyEquivalent.parse("shift-c") 41 | XCTAssertEqual(actual, .command + .shift + "c") 42 | 43 | 44 | actual = KeyEquivalent.parse("command--") 45 | XCTAssertEqual(actual, .command + "-") 46 | 47 | actual = KeyEquivalent.parse("command-\\") 48 | XCTAssertEqual(actual, .command + "\\") 49 | 50 | } 51 | 52 | // ------------------------------------- 53 | func test_can_parse_multiply_modified_character() 54 | { 55 | var actual = KeyEquivalent.parse("command-option-c") 56 | XCTAssertEqual(actual, .command + .option + "c") 57 | 58 | actual = KeyEquivalent.parse("option-shift-c") 59 | XCTAssertEqual(actual, .option + .shift + "c") 60 | 61 | actual = KeyEquivalent.parse("control-shift-c") 62 | XCTAssertEqual(actual, .control + .shift + "c") 63 | 64 | actual = KeyEquivalent.parse("command-shift-c") 65 | XCTAssertEqual(actual, .command + .shift + "c") 66 | 67 | actual = KeyEquivalent.parse("command-option-shift-c") 68 | XCTAssertEqual(actual, .command + .option + .shift + "c") 69 | 70 | actual = KeyEquivalent.parse("command-option-control-shift-c") 71 | XCTAssertEqual( 72 | actual, 73 | .command + .option + .control + .shift + "c" 74 | ) 75 | } 76 | 77 | // ------------------------------------- 78 | func test_fails_to_parse_initial_dash_followed_by_modifier() 79 | { 80 | let actual = KeyEquivalent.parse("-command-c") 81 | XCTAssertNil(actual) 82 | } 83 | 84 | // ------------------------------------- 85 | func test_fails_to_parse_repeated_dashes() 86 | { 87 | var actual = KeyEquivalent.parse("--") 88 | XCTAssertNil(actual) 89 | 90 | actual = KeyEquivalent.parse("command--c") 91 | XCTAssertNil(actual) 92 | } 93 | 94 | // ------------------------------------- 95 | func test_fails_to_parse_illegal_modifier() 96 | { 97 | var actual = KeyEquivalent.parse("capsLock-c") 98 | XCTAssertNil(actual) 99 | 100 | actual = KeyEquivalent.parse("gsag-c") 101 | XCTAssertNil(actual) 102 | 103 | actual = KeyEquivalent.parse("c-c") 104 | XCTAssertNil(actual) 105 | 106 | actual = KeyEquivalent.parse("\\-c") 107 | XCTAssertNil(actual) 108 | 109 | actual = KeyEquivalent.parse("--c") 110 | XCTAssertNil(actual) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku.xcodeproj/xcshareddata/xcschemes/Sudoku.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenuItems/ForEach.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import AppKit 22 | 23 | fileprivate let dummyMenuItem = NSMacMenuItem() 24 | 25 | // ------------------------------------- 26 | public struct ForEach 27 | { 28 | @usableFromInline 29 | internal var generator: () -> [NSMenuItem] 30 | 31 | // ------------------------------------- 32 | @inlinable 33 | public init( 34 | _ items: @escaping @autoclosure () -> S, 35 | content: @escaping (S.Element) -> MenuElement) 36 | { 37 | self.generator = 38 | { () -> [NSMenuItem] in 39 | var result = [NSMenuItem]() 40 | var tempMenu = StandardMenu() 41 | 42 | for item in items() 43 | { 44 | let menuItem = content(item) 45 | 46 | if let macMenuItem = menuItem as? MacMenuItem { 47 | result.append(macMenuItem.nsMenuItem) 48 | } 49 | else if let macMenu = menuItem as? MacMenu { 50 | result.append(macMenu.nsMenu.nsMacMenuItem!) 51 | } 52 | else 53 | { 54 | /* 55 | This is kind of a silly way to do this, but if menuItem is 56 | not a MacMenuItem and not a MacMenu, then it's some other 57 | kind of MenuElement (perhaps added in the future). In 58 | that case, it knows how to add itself to a MacMenu, so we 59 | add it, then extract it and remove it. Terribly 60 | inefficient, but it works. 61 | */ 62 | menuItem.appendSelf(to: &tempMenu) 63 | result.append(contentsOf: tempMenu.nsMenu.items) 64 | tempMenu.nsMenu.removeAllItems() 65 | } 66 | } 67 | 68 | return result 69 | } 70 | } 71 | } 72 | 73 | // ------------------------------------- 74 | extension ForEach: MenuElement 75 | { 76 | public var isItem: Bool { 77 | false 78 | } 79 | 80 | public func appendSelf(to menu: inout T) where T : MacMenu { 81 | menu.nsMenu.dynamicContent.append(generator) 82 | } 83 | 84 | public var nsMenuItem: NSMenuItem { dummyMenuItem } 85 | public var isEnabled: Bool { true } 86 | 87 | // ------------------------------------- 88 | public var title: String 89 | { 90 | get { "" } 91 | set { } 92 | } 93 | 94 | // ------------------------------------- 95 | public var isVisible: Bool 96 | { 97 | get { true } 98 | set { } 99 | } 100 | 101 | 102 | // ------------------------------------- 103 | public var canBeEnabled: Bool 104 | { 105 | get { true } 106 | set { } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenuItems/MacMenuItem.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public enum MacMenuItemState: Int 25 | { 26 | case mixed = -1 27 | case off = 0 28 | case on = 1 29 | 30 | // ------------------------------------- 31 | @usableFromInline 32 | internal var nsControlStateValue: NSControl.StateValue { 33 | .init(self.rawValue) 34 | } 35 | } 36 | 37 | // ------------------------------------- 38 | public protocol MacMenuItem: MenuElement 39 | { 40 | var nsMenuItem: NSMenuItem { get } 41 | } 42 | 43 | // ------------------------------------- 44 | extension MacMenuItem 45 | { 46 | @inlinable public var isItem: Bool { true } 47 | 48 | // ------------------------------------- 49 | @inlinable public var isChecked: Bool 50 | { 51 | get { state != .off } 52 | set { state = newValue ? .on : .off } 53 | } 54 | 55 | // ------------------------------------- 56 | @inlinable public var state: MacMenuItemState 57 | { 58 | get { MacMenuItemState(rawValue: nsMenuItem.state.rawValue)! } 59 | set { nsMenuItem.state = newValue.nsControlStateValue } 60 | } 61 | 62 | // ------------------------------------- 63 | @inlinable public mutating func toggleState() { 64 | state = isChecked ? .off : .on 65 | } 66 | 67 | // ------------------------------------- 68 | @inlinable public func with(state: MacMenuItemState) -> Self 69 | { 70 | var changedItem = self 71 | changedItem.state = state 72 | return changedItem 73 | } 74 | 75 | // ------------------------------------- 76 | @inlinable public func checked(_ status: Bool = true) -> Self 77 | { 78 | var changedItem = self 79 | changedItem.isChecked = status 80 | return changedItem 81 | } 82 | 83 | // ------------------------------------- 84 | @inlinable 85 | public func toggleStateWhenSelected(_ toggle: Bool = true) -> Self 86 | { 87 | (self.nsMenuItem as? NSMacMenuItem)?.toggleStateWhenSelected = 88 | toggle 89 | return self 90 | } 91 | 92 | // ------------------------------------- 93 | @inlinable 94 | public func updatingStateWith( 95 | updater: @escaping () -> MacMenuItemState) -> Self 96 | { 97 | (self.nsMenuItem as? NSMacMenuItem)?.stateUpdater = updater 98 | return self 99 | } 100 | 101 | // ------------------------------------- 102 | @inlinable 103 | public func indented(level: Int = 1) -> Self 104 | { 105 | nsMenuItem.indentationLevel = level 106 | return self 107 | } 108 | 109 | // ------------------------------------- 110 | @inlinable 111 | public func appendSelf(to menu: inout T) { 112 | menu.append(item: self) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Templates/Project Templates/Application/App using MacMenuBar.xctemplate/FormatMenu.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MacMenuBar 3 | 4 | // ------------------------------------- 5 | let formatMenu = StandardMenu(title: "Format") 6 | { 7 | StandardMenu(title:"Font") 8 | { 9 | TextMenuItem(title: "Show Fonts", action: .showFonts) 10 | TextMenuItem(title: "Bold", action: .bold) 11 | TextMenuItem(title: "Italic", action: .italic) 12 | TextMenuItem(title: "Underline", action: .underline) 13 | 14 | MenuSeparator() 15 | 16 | TextMenuItem(title: "Bigger", action: .enlargeFont) 17 | TextMenuItem(title: "Smaller", action: .shrinkFont) 18 | 19 | MenuSeparator() 20 | 21 | StandardMenu(title: "Kern") 22 | { 23 | // TODO: SHould these two be check-marked? 24 | TextMenuItem(title: "Use Default", action: .useStandardKerning) 25 | TextMenuItem(title: "Use None", action: .turnOffKerning) 26 | TextMenuItem(title: "Tighten", action: .tightenKerning) 27 | TextMenuItem(title: "Loosen", action: .loosenKerning) 28 | } 29 | StandardMenu(title: "Ligatures") 30 | { 31 | // TODO: SHould these three be check-marked? 32 | TextMenuItem(title: "Use Default", action: .useStandardLigatures) 33 | TextMenuItem(title: "Use None", action: .turnOffLigatures) 34 | TextMenuItem(title: "Use All", action: .useAllLigatures) 35 | } 36 | StandardMenu(title: "Baseline") 37 | { 38 | // TODO: SHould these four be check-marked? 39 | TextMenuItem(title: "Use Default", action: .useDefaultBaseline) 40 | TextMenuItem(title: "Superscript", action: .superscript) 41 | TextMenuItem(title: "Subscript", action: .subscript) 42 | TextMenuItem(title: "Raise", action: .raiseBaseline) 43 | TextMenuItem(title: "Lower", action: .lowerBaseline) 44 | } 45 | 46 | MenuSeparator() 47 | 48 | TextMenuItem(title: "Show Colors", action: .showColors) 49 | 50 | MenuSeparator() 51 | 52 | TextMenuItem(title: "Copy Style", action: .copyStyle) 53 | TextMenuItem(title: "Paste Style", action: .pasteStyle) 54 | } 55 | 56 | StandardMenu(title:"Text") 57 | { 58 | TextMenuItem(title: "Align Left", action: .alignLeft) 59 | TextMenuItem(title: "Center", action: .alignCenter) 60 | TextMenuItem(title: "Justify", action: .alignJustified) 61 | TextMenuItem(title: "Align Right", action: .alignRight) 62 | 63 | MenuSeparator() 64 | 65 | StandardMenu(title: "Writing Direction") 66 | { 67 | TextMenuItem(title: "Paragraph").enabled(false) 68 | TextMenuItem( 69 | title: "Default", 70 | action: .useNaturalBaseWritingWritingDirection 71 | ).indented() 72 | TextMenuItem( 73 | title: "Left to Right", 74 | action: .useLeftToRightBaseWritingDirection 75 | ).indented() 76 | TextMenuItem( 77 | title: "Right to Left", 78 | action: .useRightToLeftBaseWritingDirection 79 | ).indented() 80 | 81 | MenuSeparator() 82 | 83 | TextMenuItem(title: "Selection").enabled(false) 84 | TextMenuItem( 85 | title: "Default", 86 | action: .makeNaturalBaseWritingDirection 87 | ).indented() 88 | TextMenuItem( 89 | title: "Left to Right", 90 | action: .makeLeftToRightDirection 91 | ).indented() 92 | TextMenuItem( 93 | title: "Right to Left", 94 | action: .makeRightToLeftDirection 95 | ).indented() 96 | } 97 | 98 | MenuSeparator() 99 | 100 | TextMenuItem(title: "Show Ruler", action: .showRuler) 101 | TextMenuItem(title: "Copy Ruler", action: .copyRuler) 102 | TextMenuItem(title: "PasteRuler", action: .pasteRuler) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/Actions/Action.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public protocol Action 25 | { 26 | var keyEquivalent: KeyEquivalent? { get set } 27 | var isEnabled: Bool { get } 28 | var canBeEnabled: Bool { get set } 29 | var enabledValidator: (() -> Bool)? { get set } 30 | 31 | // ------------------------------------- 32 | /** 33 | - Returns: `true` if the action was performed; otherwise, `false` 34 | */ 35 | func performActionSelf( 36 | on target: ActionResponder?, 37 | for sender: Any?) -> Bool 38 | } 39 | 40 | // ------------------------------------- 41 | public extension Action 42 | { 43 | static var none: Action { NoAction() as Action } 44 | 45 | // ------------------------------------- 46 | /** 47 | Determine if the specified `KeyEquivalent` is bound to this `Action` 48 | 49 | - Parameter keyEquivalent: The `KeyEquivalent` to check 50 | 51 | - Returns: `true` if this `Action` is bound to the specified 52 | `KeyEquivalent`; otherwise, `false` 53 | */ 54 | func responds(to keyEquivalent: KeyEquivalent) -> Bool 55 | { 56 | guard let k = self.keyEquivalent, k.key != nullChar else { 57 | return false 58 | } 59 | return keyEquivalent == k 60 | } 61 | 62 | // ------------------------------------- 63 | /** 64 | Perform this `Action` on `target` or on the first `ActionResponder` in the 65 | responder chain that will respond to it. 66 | 67 | If `target` is specified, it is checked first. If it responds to this 68 | `Action` it's `performAction(_:, for:)` method is called.. If it does not 69 | or is `nil`, then the current responder chain is searched, starting from 70 | the application's `NSApp.keyWindow`, and ending with `NSApp` itself. 71 | 72 | - Returns: `true` if the action was performed; otherwise, `false` 73 | */ 74 | func performAction(on target: ActionResponder?, for sender: Any?) -> Bool 75 | { 76 | guard isEnabled, let target = findResponder(target: target) else { 77 | return false 78 | } 79 | 80 | return performActionSelf(on: target, for: sender) 81 | } 82 | 83 | // ------------------------------------- 84 | /** 85 | Searches the responder chain for an `ActionResponder` for the this `Action` 86 | 87 | If `target` is specified, it is checked first. If it responds to this 88 | `Action` it is returned. If it does not or is `nil`, then the current 89 | responder chain is searched, starting from the application's 90 | `NSApp.keyWindow`, and ending with `NSApp` itself. 91 | 92 | - Parameter target: an optional `ActionResponder` to start the search. 93 | 94 | - Returns: A responder that responds to the specified action, or `nil` if 95 | none respond to this `Action`. 96 | */ 97 | func findResponder(target: ActionResponder?) -> ActionResponder? 98 | { 99 | if target?.responds(to: self) ?? false { return target } 100 | 101 | for responder in ResponderChain() { 102 | if responder.responds(to: self) { return responder } 103 | } 104 | 105 | return nil 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenuItems/TextMenuItem.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public struct TextMenuItem 25 | { 26 | /** 27 | - Parameter sender: The object that triggered the action. 28 | */ 29 | public typealias ActionClosure = ClosureAction.ActionClosure 30 | 31 | public private(set) var nsMenuItem: NSMenuItem 32 | 33 | @inlinable public var action: Action 34 | { 35 | get { (nsMenuItem as? NSMacMenuItem)!._action! } 36 | set { (nsMenuItem as? NSMacMenuItem)!._action! = newValue } 37 | } 38 | 39 | // ------------------------------------- 40 | @inlinable public var keyEquivalent: KeyEquivalent 41 | { 42 | get { KeyEquivalent(from: nsMenuItem) } 43 | set 44 | { 45 | nsMenuItem.keyEquivalent = String(newValue.key) 46 | nsMenuItem.keyEquivalentModifierMask = newValue.modifiers 47 | } 48 | } 49 | 50 | // ------------------------------------- 51 | /** 52 | Initializes a new MacMenuItem. The menu item is visible and enabled by default. 53 | 54 | - Parameters: 55 | - title: `String` to be displayed for this menu item. 56 | - keyEquivalent: `String` indicating the keyboard key combination short-cut for this menu item. 57 | - action: Closure to be called when this menu item is selected. 58 | */ 59 | // ------------------------------------- 60 | @inlinable 61 | public init( 62 | title: String, 63 | keyEquivalent: KeyEquivalent = .none, 64 | action: @escaping ActionClosure) 65 | { 66 | self.init( 67 | title: title, 68 | action: ClosureAction(keyEquivalent: keyEquivalent, closure: action) 69 | ) 70 | } 71 | 72 | // ------------------------------------- 73 | @inlinable 74 | public init( 75 | title: String, 76 | keyEquivalent: KeyEquivalent, 77 | action: Selector) 78 | { 79 | self.init( 80 | title: title, 81 | action: SelectorAction( 82 | keyEquivalent: keyEquivalent, 83 | selector: action 84 | ) 85 | ) 86 | } 87 | 88 | // ------------------------------------- 89 | @inlinable 90 | public init(action: Action) 91 | { 92 | self.init(title: "", action: action) 93 | canBeEnabled = true 94 | isVisible = true 95 | } 96 | 97 | // ------------------------------------- 98 | @inlinable 99 | public init(title: String, action: Action = NoAction()) { 100 | self.init(from: NSMacMenuItem(title: title, action: action)) 101 | } 102 | 103 | // ------------------------------------- 104 | @inlinable 105 | public init(title: String, action: StandardMenuItemAction) 106 | { 107 | self.init(action: action) 108 | self.title = title 109 | } 110 | 111 | // ------------------------------------- 112 | @usableFromInline 113 | internal init(from nsMacMenuItem: NSMacMenuItem) { 114 | self.nsMenuItem = nsMacMenuItem 115 | } 116 | } 117 | 118 | // ------------------------------------- 119 | extension TextMenuItem: ActionableMenuItem 120 | { 121 | // ------------------------------------- 122 | @inlinable public var title: String 123 | { 124 | get { nsMenuItem.title } 125 | set { nsMenuItem.title = newValue } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/AppKit Subclasses/DynamicNSMenuContent.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | // 21 | 22 | import AppKit 23 | 24 | // ------------------------------------- 25 | struct DynamicNSMenuContent 26 | { 27 | // ------------------------------------- 28 | struct Group 29 | { 30 | let generator: () -> [NSMenuItem] 31 | 32 | // ------------------------------------- 33 | init(with generator: @escaping () -> [NSMenuItem]) { 34 | self.generator = generator 35 | } 36 | 37 | // ------------------------------------- 38 | func addAll(to menu: NSMacMenu) { 39 | generator().forEach { menu.addItem($0) } 40 | } 41 | } 42 | 43 | var groups: [Group] = [] 44 | 45 | // ------------------------------------- 46 | mutating func append(_ item: NSMenuItem) { 47 | groups.append(Group { [item] }) 48 | } 49 | 50 | // ------------------------------------- 51 | mutating func append(_ submenu: NSMenu) 52 | { 53 | groups.append( 54 | Group 55 | { 56 | if let item = submenu.parentItem { return [item] } 57 | return [] 58 | } 59 | ) 60 | } 61 | 62 | // ------------------------------------- 63 | mutating func append(_ elements: @escaping () -> [NSMenuItem]) { 64 | groups.append(Group(with: elements)) 65 | } 66 | 67 | // ------------------------------------- 68 | func rebuild(for menu: NSMacMenu) 69 | { 70 | menu.rebuilding = true 71 | defer { menu.rebuilding = false } 72 | 73 | /* 74 | Annoyingly, macOS inserts its own menu items into our menus. We 75 | already refuse the insert the ones that use selectors we already 76 | implement menu items for, so we don't get duplicates, but we allow it 77 | for ones we don't so that users get the menus they expect for their 78 | macOS version. 79 | 80 | The problem is that injected menus aren't in our groups, so we have to 81 | save any menu items that aren't NSMacMenuItems and then append them to 82 | the end afterwards. 83 | 84 | Currently, macOS always inserts those at the end of the menu. If some 85 | future macOS version inserts them elsewhere... well they're about to be 86 | re-arranged. 87 | */ 88 | var savedItems = menu.items.filter { !($0 is NSMacMenuItem) } 89 | 90 | menu.removeAllItems() 91 | groups.forEach { $0.addAll(to: menu) } 92 | 93 | if savedItems.count > 0 94 | { 95 | // We don't want our menu ending with a separator 96 | if savedItems.last?.isSeparatorItem == true { 97 | savedItems.removeLast() 98 | } 99 | 100 | // Make sure saved items are separated from rest of menu 101 | if savedItems.first?.isSeparatorItem == false { 102 | menu.addItem(NSMacMenuItem.separator()) 103 | } 104 | 105 | savedItems.forEach { menu.addItem($0) } 106 | } 107 | } 108 | } 109 | 110 | // ------------------------------------- 111 | fileprivate extension NSMenu 112 | { 113 | // ------------------------------------- 114 | var parentItem: NSMenuItem? 115 | { 116 | guard let index = supermenu?.indexOfItem(withSubmenu: self) else { 117 | return nil 118 | } 119 | 120 | return supermenu?.items[index] 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Examples/Sudoku/Sudoku/Views/OverlaySheets/ThemeEditor/NSViewRepresentables/FontFamilyPopupButton.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import SwiftUI 22 | 23 | fileprivate let _fontSize: CGFloat = 11 24 | 25 | // ------------------------------------- 26 | fileprivate func font(from family: String) -> NSFont? 27 | { 28 | NSFontManager.shared.font( 29 | withFamily: family, 30 | traits: [], weight: 5, 31 | size: _fontSize 32 | ) 33 | } 34 | 35 | // ------------------------------------- 36 | fileprivate let fontList = NSFontManager.shared.availableFontFamilies 37 | .map { font(from: $0) }.filter { $0 != nil }.map { $0! } 38 | 39 | // ------------------------------------- 40 | fileprivate var attributedFontNames: [String: NSAttributedString] = 41 | { 42 | var map = [String: NSAttributedString]() 43 | map.reserveCapacity(fontList.count) 44 | 45 | for font in fontList 46 | { 47 | guard let familyName = font.familyName else { continue } 48 | map[familyName] = familyName.attributedString(font) 49 | } 50 | return map 51 | }() 52 | 53 | // ------------------------------------- 54 | struct FontFamilyPopupButton: PopupButtonProtocol 55 | { 56 | typealias Value = NSFont 57 | typealias ValueContainer = ValueContainer 58 | 59 | static var fontSize: CGFloat { _fontSize } 60 | 61 | var width: CGFloat 62 | var height: CGFloat 63 | var valueContainer: Binding 64 | var valuePath: ValuePath 65 | var content: () -> [NSFont] 66 | 67 | // ------------------------------------- 68 | init( 69 | width: CGFloat, 70 | height: CGFloat, 71 | valuePath: ValuePath, 72 | in container: Binding, 73 | content: @escaping () -> [NSFont]) 74 | { 75 | self.width = width 76 | self.height = height 77 | self.valueContainer = container 78 | self.valuePath = valuePath 79 | self.content = content 80 | } 81 | 82 | // ------------------------------------- 83 | init( 84 | width: CGFloat, 85 | height: CGFloat, 86 | valuePath: ValuePath, 87 | in container: Binding) 88 | { 89 | self.init( 90 | width: width, 91 | height: height, 92 | valuePath: valuePath, 93 | in: container) { fontList } 94 | } 95 | 96 | // ------------------------------------- 97 | func itemsAreEqual(_ value1: Value, _ value2: Value) -> Bool { 98 | return value1.familyName == value2.familyName 99 | } 100 | 101 | // ------------------------------------- 102 | func attributedItemTitle(from value: Value) -> NSAttributedString? 103 | { 104 | guard let familyName = value.familyName else { return nil } 105 | if let attributedName = attributedFontNames[familyName] { 106 | return attributedName 107 | } 108 | 109 | let attributedName = familyName.attributedString(value) 110 | attributedFontNames[familyName] = attributedName 111 | return attributedName 112 | } 113 | 114 | // ------------------------------------- 115 | func itemTitle(from value: Value) -> String? { nil } 116 | 117 | // ------------------------------------- 118 | func value(for itemTitle: String) -> Value? 119 | { 120 | return NSFontManager.shared.font( 121 | withFamily: itemTitle, 122 | traits: [], 123 | weight: 5, 124 | size: currentValue.pointSize 125 | ) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/MacMenuBar/MacMenuItems/ActionableMenuItem.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Chip Jarred 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | import Cocoa 22 | 23 | // ------------------------------------- 24 | public protocol ActionableMenuItem: MacMenuItem 25 | { 26 | var keyEquivalent: KeyEquivalent { get set } 27 | var action: Action { get set } 28 | 29 | init(action: Action) 30 | } 31 | 32 | // ------------------------------------- 33 | extension ActionableMenuItem 34 | { 35 | public init(action: StandardMenuItemAction) 36 | { 37 | let (selector, keyEquivalent) = action.selectorAndKeyEquivalent 38 | self.init( 39 | action: SelectorAction( 40 | keyEquivalent: keyEquivalent, 41 | selector: selector 42 | ) 43 | ) 44 | } 45 | 46 | // ------------------------------------- 47 | public init( 48 | keyEquivalent: KeyEquivalent = .none, 49 | action: @escaping ClosureAction.ActionClosure) 50 | { 51 | self.init(action: ClosureAction(action)) 52 | self.keyEquivalent = keyEquivalent 53 | } 54 | 55 | // ------------------------------------- 56 | public mutating func setStandardAction(_ action: StandardMenuItemAction) 57 | { 58 | let (selector, keyEquivalent) = action.selectorAndKeyEquivalent 59 | self.action = SelectorAction( 60 | keyEquivalent: keyEquivalent, 61 | selector: selector 62 | ) 63 | } 64 | 65 | // ------------------------------------- 66 | @inlinable public var isVisible: Bool 67 | { 68 | get { !nsMenuItem.isHidden } 69 | set { nsMenuItem.isHidden = !newValue } 70 | } 71 | 72 | // ------------------------------------- 73 | @inlinable public var canBeEnabled: Bool 74 | { 75 | get { (nsMenuItem as? NSMacMenuItem)?.canBeEnabled ?? true } 76 | set { (nsMenuItem as? NSMacMenuItem)?.canBeEnabled = newValue } 77 | } 78 | 79 | // ------------------------------------- 80 | @inlinable public var isEnabled: Bool { nsMenuItem.isEnabled } 81 | 82 | // ------------------------------------- 83 | @inlinable public func enabled(_ enable: Bool = true) -> Self 84 | { 85 | var item = self 86 | item.canBeEnabled = enable 87 | return item 88 | } 89 | 90 | // ------------------------------------- 91 | @inlinable public func enabledWhen( 92 | _ validator: @escaping () -> Bool) -> Self 93 | { 94 | if let item = nsMenuItem as? NSMacMenuItem { 95 | item.enabledValidator = validator 96 | } 97 | return self 98 | } 99 | 100 | // ------------------------------------- 101 | @inlinable public func visible(_ makeVisible: Bool = true) -> Self 102 | { 103 | var item = self 104 | item.isVisible = makeVisible 105 | return item 106 | } 107 | 108 | // ------------------------------------- 109 | @inlinable public func beforeAction( 110 | do code: @escaping (NSMenuItem) -> Void) -> Self 111 | { 112 | if let item = nsMenuItem as? NSMacMenuItem { 113 | item.preActionClosure = code 114 | } 115 | 116 | return self 117 | } 118 | 119 | // ------------------------------------- 120 | @inlinable public func afterAction( 121 | do code: @escaping (NSMenuItem) -> Void) -> Self 122 | { 123 | if let item = nsMenuItem as? NSMacMenuItem { 124 | item.postActionClosure = code 125 | } 126 | 127 | return self 128 | } 129 | 130 | // ------------------------------------- 131 | @inlinable public func updatingTitleWith( 132 | _ updater: @escaping () -> String) -> Self 133 | { 134 | if let item = nsMenuItem as? NSMacMenuItem { 135 | item.titleUpdater = updater 136 | } 137 | return self 138 | } 139 | } 140 | --------------------------------------------------------------------------------