├── 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 |
--------------------------------------------------------------------------------