├── Macropad Toolbox
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── SpecialKeyBackground.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Utility
│ ├── UIConstants.swift
│ ├── ReorderableForEach.swift
│ └── Extensions.swift
├── Core Data
│ ├── Macropad_Toolbox.xcdatamodeld
│ │ ├── .xccurrentversion
│ │ └── Macropad_Toolbox.xcdatamodel
│ │ │ └── contents
│ ├── JSONExportSupport.swift
│ ├── Configuration.swift
│ ├── Persistence.swift
│ ├── Key.swift
│ ├── RotaryEncoder.swift
│ ├── Page.swift
│ └── Macro.swift
├── Macropad_Toolbox.entitlements
├── Macropad_ToolboxApp.swift
├── ContentView.swift
├── PageInvocationEditView.swift
├── KeyGridElementView.swift
├── KeyDetailView.swift
├── ModifierSelectionView.swift
├── Keycodes.swift
├── ConfigurationListView.swift
├── RotaryConfigView.swift
├── PageDetailView.swift
└── PageListView.swift
├── Macropad Toolbox.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── daniloc.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── README.md
├── Macropad ToolboxUITests
├── Macropad_ToolboxUITestsLaunchTests.swift
└── Macropad_ToolboxUITests.swift
├── Macropad ToolboxTests
└── Macropad_ToolboxTests.swift
├── LICENSE
└── .gitignore
/Macropad Toolbox/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Macropad Toolbox.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Macropad Toolbox/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 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Utility/UIConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIConstants.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/20/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension Color {
11 | static let specialKeyBackground = Color("SpecialKeyBackground")
12 | }
13 |
--------------------------------------------------------------------------------
/Macropad Toolbox.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Core Data/Macropad_Toolbox.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | Macropad_Toolbox.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Macropad_Toolbox.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-write
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # macropad_toolbox
2 | A macOS GUI for [Macropad](https://www.adafruit.com/product/5128) key layouts
3 |
4 | Requires appropriate CircuitPython code to parse configuration files on the Macropad: https://github.com/daniloc/macropad_configurable
5 |
6 | 
7 |
--------------------------------------------------------------------------------
/Macropad Toolbox.xcodeproj/xcuserdata/daniloc.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Macropad Toolbox.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Macropad_ToolboxApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Macropad_ToolboxApp.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct Macropad_ToolboxApp: App {
12 | let persistenceController = PersistenceController.shared
13 |
14 | var body: some Scene {
15 | WindowGroup {
16 | ContentView()
17 | .environment(\.managedObjectContext, persistenceController.container.viewContext)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Core Data/JSONExportSupport.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONExportSupport.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | //adapted via: https://www.donnywals.com/using-codable-with-core-data-and-nsmanagedobject/
11 |
12 | enum DecoderConfigurationError: Error {
13 | case missingManagedObjectContext
14 | }
15 |
16 | enum ExportError: Error {
17 | case unableToCapturePages
18 | }
19 |
20 | extension CodingUserInfoKey {
21 | static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
22 | }
23 |
24 | extension Decoder {
25 | var managedObjectContext: NSManagedObjectContext? {
26 | return self.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Assets.xcassets/SpecialKeyBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.702",
9 | "green" : "0.702",
10 | "red" : "0.702"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.387",
27 | "green" : "0.387",
28 | "red" : "0.387"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Macropad ToolboxUITests/Macropad_ToolboxUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Macropad_ToolboxUITestsLaunchTests.swift
3 | // Macropad ToolboxUITests
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import XCTest
9 |
10 | class Macropad_ToolboxUITestsLaunchTests: XCTestCase {
11 |
12 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
13 | true
14 | }
15 |
16 | override func setUpWithError() throws {
17 | continueAfterFailure = false
18 | }
19 |
20 | func testLaunch() throws {
21 | let app = XCUIApplication()
22 | app.launch()
23 |
24 | // Insert steps here to perform after app launch but before taking a screenshot,
25 | // such as logging into a test account or navigating somewhere in the app
26 |
27 | let attachment = XCTAttachment(screenshot: app.screenshot())
28 | attachment.name = "Launch Screen"
29 | attachment.lifetime = .keepAlways
30 | add(attachment)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Macropad Toolbox/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import SwiftUI
9 | import CoreData
10 |
11 | struct ContentView: View {
12 | @Environment(\.managedObjectContext) private var viewContext
13 |
14 | @FetchRequest(
15 | sortDescriptors: [NSSortDescriptor(keyPath: \Configuration.modificationDate, ascending: true)],
16 | animation: .default)
17 | private var items: FetchedResults
18 |
19 | var body: some View {
20 | NavigationView {
21 |
22 | ConfigurationListView()
23 |
24 | Text("Select a configuration")
25 |
26 | Text("Select a page")
27 | }
28 | }
29 |
30 |
31 | }
32 |
33 | struct ContentView_Previews: PreviewProvider {
34 | static var previews: some View {
35 | ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Macropad ToolboxTests/Macropad_ToolboxTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Macropad_ToolboxTests.swift
3 | // Macropad ToolboxTests
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import XCTest
9 | @testable import Macropad_Toolbox
10 |
11 | class Macropad_ToolboxTests: 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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Danilo Campos
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 |
--------------------------------------------------------------------------------
/Macropad Toolbox/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 |
--------------------------------------------------------------------------------
/Macropad ToolboxUITests/Macropad_ToolboxUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Macropad_ToolboxUITests.swift
3 | // Macropad ToolboxUITests
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import XCTest
9 |
10 | class Macropad_ToolboxUITests: 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, watchOS 7.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Macropad Toolbox/PageInvocationEditView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InvocationSelectionView.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/22/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PageInvocationEditView: View {
11 |
12 | @ObservedObject var page: Page
13 |
14 |
15 | var body: some View {
16 | HStack {
17 | Text("Activation:")
18 | Spacer()
19 |
20 | VStack(spacing: 2) {
21 | ForEach((0...page.invocation!.count - 1), id: \.self) { index in
22 | Button {
23 |
24 | if page.invocation![index] == 1 {
25 | page.invocation![index] = 0
26 | } else {
27 | page.invocation![index] = 1
28 | }
29 |
30 | page.pendingInvocationChange()
31 |
32 | } label: {
33 | Group {
34 | if page.invocation![index] == 0 {
35 | Image(systemName: "square")
36 | } else {
37 | Image(systemName: "square.fill")
38 | }
39 | }
40 | .font(.system(size: 16))
41 |
42 | }
43 | .buttonStyle(.borderless)
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | struct InvocationSelectionView_Previews: PreviewProvider {
51 | static var previews: some View {
52 | PageInvocationEditView(page: Page(context: PersistenceController.preview.container.viewContext))
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Macropad Toolbox/KeyGridElementView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyGridElementView.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct KeyGridElementView: View {
11 |
12 | @ObservedObject var key: Key
13 | @Binding var selectedKey: Key?
14 |
15 | var borderColor: Color {
16 | if key == selectedKey {
17 | return .yellow
18 | } else {
19 | return .black
20 | }
21 | }
22 |
23 | var borderWidth: Double {
24 | if key == selectedKey {
25 | return 3
26 | } else {
27 | return 1
28 | }
29 | }
30 |
31 | var body: some View {
32 |
33 | ZStack {
34 |
35 | if key == selectedKey {
36 | Color.primary
37 | .opacity(0.2)
38 | .cornerRadius(4)
39 | }
40 |
41 | VStack {
42 |
43 | ZStack {
44 | key.color
45 |
46 | RadialGradient(colors: [.clear, .init(white: 0.5, opacity: 0.25)].reversed(), center: .center, startRadius: 1, endRadius: 25)
47 | }
48 | .frame(width: 60, height: 60)
49 | .border(.black, width: 1)
50 | .cornerRadius(8)
51 |
52 |
53 | if key.label?.count == 0 {
54 | Text("Blank")
55 | .foregroundColor(.secondary)
56 | } else {
57 | Text(key.label ?? "")
58 | }
59 |
60 |
61 | }
62 | .padding(4)
63 | .onTapGesture {
64 | selectedKey = key
65 | }
66 | }
67 | }
68 | }
69 |
70 | struct KeyGridElementView_Previews: PreviewProvider {
71 | static var previews: some View {
72 | KeyGridElementView(key: Key(context: PersistenceController.previewObjectContext), selectedKey: .constant(nil))
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Macropad Toolbox/KeyDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyDetailView.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct MacroEditView: View {
11 | @ObservedObject var macro: Macro
12 |
13 | var macroInputEditor: some View {
14 | VStack {
15 | HStack {
16 | Text("Text output:")
17 | TextField("Output", text: $macro.textContent ?? "", prompt: Text("Text typed by this key"))
18 |
19 | }
20 |
21 | ModifierSelectionView(macro: macro)
22 | }
23 | }
24 |
25 | var body: some View {
26 |
27 | VStack(alignment: .leading) {
28 |
29 | if macro.preview.count == 0 {
30 | Text("[No output]")
31 | .foregroundColor(.secondary)
32 | } else {
33 | Text(macro.preview)
34 | .foregroundColor(.secondary)
35 | }
36 |
37 | Divider()
38 |
39 | macroInputEditor
40 |
41 |
42 |
43 |
44 |
45 | }
46 | }
47 | }
48 |
49 | struct KeyDetailView: View {
50 |
51 | @ObservedObject var key: Key
52 |
53 | var body: some View {
54 | VStack(alignment: .leading) {
55 |
56 | HStack {
57 |
58 | TextField("Label", text: $key.label ?? "", prompt: Text("Key label"))
59 |
60 | Spacer()
61 |
62 | Button {
63 | key.reset()
64 | } label: {
65 | Label("Reset", systemImage: "clear")
66 | }
67 |
68 |
69 | }
70 |
71 | ColorPicker("Color", selection: $key.color)
72 |
73 | if let macro = key.macro {
74 | MacroEditView(macro: macro)
75 | }
76 |
77 | }
78 | }
79 | }
80 |
81 | struct KeyDetailView_Previews: PreviewProvider {
82 | static var previews: some View {
83 | KeyDetailView(key: Key(context: PersistenceController.preview.container.viewContext))
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Core Data/Configuration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Configuration+CoreDataClass.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | @objc(Configuration)
13 | public class Configuration: NSManagedObject {
14 | public override func awakeFromInsert() {
15 | super.awakeFromInsert()
16 |
17 | name = "New Configuration"
18 |
19 | let page = Page(context: self.managedObjectContext!)
20 | page.name = "New Page"
21 |
22 | addToPages(page)
23 | }
24 |
25 | func claim(invocation: [Int], page: Page) {
26 | for page in self.pages?.array as! [Page] {
27 | if page.invocation == invocation {
28 | page.clearInvocation()
29 | }
30 | }
31 |
32 | page.invocation = invocation
33 | self.objectWillChange.send()
34 | }
35 |
36 | func jsonData() throws -> Data {
37 |
38 | guard let pages = pages?.array as? [Page] else {
39 | throw ExportError.unableToCapturePages
40 | }
41 |
42 | let data = try JSONEncoder().encode(pages)
43 |
44 | return data
45 |
46 | }
47 |
48 | func movePages(indices: IndexSet, destination: Int) {
49 |
50 | var pagesArray = pages?.array ?? []
51 |
52 | pagesArray.move(fromOffsets: indices, toOffset: destination)
53 |
54 | self.pages = NSOrderedSet(array: pagesArray)
55 |
56 | logUpdate()
57 | }
58 |
59 | func deletePage(_ page: Page) {
60 |
61 | guard let context = self.managedObjectContext else { return }
62 |
63 | page.configuration = nil
64 |
65 | context.delete(page)
66 | context.attemptSaveLoggingErrors()
67 |
68 | logUpdate()
69 | }
70 |
71 | func delete() {
72 | guard let context = self.managedObjectContext else { return }
73 |
74 | context.delete(self)
75 | context.attemptSaveLoggingErrors()
76 | }
77 |
78 | func logUpdate() {
79 | self.modificationDate = Date()
80 | self.managedObjectContext?.attemptSaveLoggingErrors()
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Core Data/Persistence.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Persistence.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import CoreData
9 |
10 | struct PersistenceController {
11 | static let shared = PersistenceController()
12 |
13 | static var previewObjectContext: NSManagedObjectContext {
14 | return Self.preview.container.viewContext
15 | }
16 |
17 | static var viewContext: NSManagedObjectContext {
18 | return Self.shared.container.viewContext
19 | }
20 |
21 | static var preview: PersistenceController = {
22 | let result = PersistenceController(inMemory: true)
23 | let viewContext = result.container.viewContext
24 | for index in 0..<10 {
25 | let newItem = Configuration(context: viewContext)
26 | newItem.modificationDate = Date()
27 | newItem.name = "Configuration \(index)"
28 | }
29 | do {
30 | try viewContext.save()
31 | } catch {
32 | // Replace this implementation with code to handle the error appropriately.
33 | // 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.
34 | let nsError = error as NSError
35 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
36 | }
37 | return result
38 | }()
39 |
40 | let container: NSPersistentContainer
41 |
42 | init(inMemory: Bool = false) {
43 | container = NSPersistentContainer(name: "Macropad_Toolbox")
44 | if inMemory {
45 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
46 | }
47 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in
48 | if let error = error as NSError? {
49 |
50 | fatalError("Unresolved error \(error), \(error.userInfo)")
51 | }
52 | })
53 | }
54 | }
55 |
56 | extension NSManagedObjectContext {
57 | func attemptSaveLoggingErrors() {
58 |
59 | do {
60 | try save()
61 | } catch {
62 | print("⚠️ Error saving context:\n\(error)")
63 | }
64 |
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Macropad Toolbox/ModifierSelectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModifierSelectionView.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/20/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ModifierSelectionView: View {
11 |
12 | @ObservedObject var macro: Macro
13 |
14 | var items: [GridItem] {
15 | Array(repeating: .init(.fixed(90), spacing: 3), count: 4)
16 | }
17 |
18 | func itemLabel(_ content: String) -> some View {
19 | Text(content)
20 | .font(.system(size: 11, design: .monospaced))
21 | .foregroundColor(.primary)
22 | .lineLimit(1)
23 | .padding(2)
24 | }
25 |
26 | fileprivate func modifierPickerItem(_ keycode: Macro.SpecialKeyInput) -> some View {
27 | return ZStack {
28 |
29 | if macro.specialKeys.contains(keycode) {
30 | Color.blue
31 | } else {
32 | Color.specialKeyBackground
33 | }
34 |
35 | itemLabel(keycode.uiLabel)
36 |
37 | }
38 | .onTapGesture {
39 | macro.toggleKey(keycode)
40 | }
41 | }
42 |
43 | var body: some View {
44 | ScrollView {
45 | LazyVGrid(columns: items, spacing: 3) {
46 |
47 | Section("Media Keys") {
48 | ForEach(AdafruitHIDPythonMediaControlCode.allCases) { keycode in
49 |
50 | modifierPickerItem(Macro.SpecialKeyInput.consumerControl(keycode))
51 | .id(Macro.SpecialKeyInput.consumerControl(keycode).id)
52 | }
53 | }
54 |
55 | Section("Modifier Keys") {
56 | ForEach(AdafruitPythonHIDKeycode.allCases) { keycode in
57 |
58 | modifierPickerItem(Macro.SpecialKeyInput.modifier(keycode))
59 | .id(Macro.SpecialKeyInput.modifier(keycode).id)
60 |
61 | }
62 | }
63 |
64 |
65 | }
66 | }
67 | }
68 | }
69 |
70 | struct ModifierSelectionView_Previews: PreviewProvider {
71 | static var previews: some View {
72 | ModifierSelectionView(macro: Macro(context: PersistenceController.previewObjectContext))
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Core Data/Key.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Key+CoreDataClass.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 | //
8 |
9 | import SwiftUI
10 | import CoreData
11 |
12 | @objc(Key)
13 | public class Key: NSManagedObject, Codable {
14 |
15 | public override func awakeFromInsert() {
16 | super.awakeFromInsert()
17 |
18 | reset()
19 |
20 | }
21 |
22 |
23 |
24 | var color: Color {
25 | get {
26 | return Color(hex: self.colorHex ?? "000000")
27 | }
28 |
29 | set {
30 |
31 | self.colorHex = newValue.toHex
32 | self.objectWillChange.send()
33 |
34 | self.page?.configuration?.logUpdate()
35 |
36 | }
37 | }
38 |
39 | func reset() {
40 | self.color = Color(white: 0.0, opacity: 1)
41 | self.label = ""
42 |
43 | self.macro = Macro(context: self.managedObjectContext!)
44 |
45 | self.macro?.textContent = ""
46 | }
47 |
48 | //MARK: - Codable
49 |
50 | enum CodingKeys: CodingKey {
51 | case color,
52 | label,
53 | macro
54 | }
55 |
56 | public required convenience init(from decoder: Decoder) throws {
57 | guard let context = decoder.managedObjectContext else {
58 | throw DecoderConfigurationError.missingManagedObjectContext
59 | }
60 |
61 | self.init(context: context)
62 |
63 | let container = try decoder.container(keyedBy: CodingKeys.self)
64 |
65 | self.colorHex = try container.decode(String.self, forKey: .color)
66 |
67 | self.label = try container.decode(String.self, forKey: .label)
68 |
69 | self.macro = try container.decode(Macro.self, forKey: .macro)
70 | }
71 |
72 | public func encode(to encoder: Encoder) throws {
73 | var container = encoder.container(keyedBy: CodingKeys.self)
74 |
75 | guard let rawColor = color.cgColor, let convertedColor = NSColor(cgColor: rawColor) else {
76 | return
77 | }
78 |
79 | var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0
80 |
81 | convertedColor.getHue(&h, saturation: &s, brightness: &b, alpha: nil)
82 |
83 | let cappedBrightness = min(b, 0.25)
84 |
85 |
86 | let adjustedColor = Color(hue: h, saturation: s, brightness: cappedBrightness)
87 |
88 | if let hex = adjustedColor.toHex {
89 | try container.encode(hex, forKey: .color)
90 | }
91 |
92 | try container.encodeIfPresent(label, forKey: .label)
93 |
94 | try container.encodeIfPresent(macro, forKey: .macro)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Core Data/RotaryEncoder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RotaryEncoder+CoreDataClass.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/22/22.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | @objc(RotaryEncoder)
13 | public class RotaryEncoder: NSManagedObject, Codable {
14 |
15 | enum Position: Int16 {
16 | case left,
17 | right
18 |
19 | func descriptionString() -> String {
20 | switch self {
21 | case .left:
22 | return "Left Rotary"
23 | case .right:
24 | return "Right Rotary"
25 |
26 | }
27 | }
28 | }
29 |
30 | var position: Position {
31 | get {
32 | return Position(rawValue: positionInt)!
33 | }
34 |
35 | set {
36 | positionInt = newValue.rawValue
37 | }
38 | }
39 |
40 | public override func awakeFromInsert() {
41 | super.awakeFromInsert()
42 |
43 | self.leftTurn = Macro(context: self.managedObjectContext!)
44 | self.rightTurn = Macro(context: self.managedObjectContext!)
45 | self.press = Macro(context: self.managedObjectContext!)
46 | }
47 |
48 | //MARK: - Codable
49 |
50 | enum CodingKeys: CodingKey {
51 | case position,
52 | leftTurn,
53 | rightTurn,
54 | press
55 |
56 | }
57 |
58 | public required convenience init(from decoder: Decoder) throws {
59 | guard let context = decoder.managedObjectContext else {
60 | throw DecoderConfigurationError.missingManagedObjectContext
61 | }
62 |
63 | self.init(context: context)
64 |
65 | let container = try decoder.container(keyedBy: CodingKeys.self)
66 |
67 | self.leftTurn = try container.decode(Macro.self, forKey: .leftTurn)
68 | self.rightTurn = try container.decode(Macro.self, forKey: .rightTurn)
69 | self.press = try container.decode(Macro.self, forKey: .press)
70 |
71 | let positionString = try container.decode(String.self, forKey: .position)
72 |
73 | if positionString == Position.left.descriptionString() {
74 | self.position = .left
75 | } else {
76 | self.position = .right
77 | }
78 |
79 | }
80 |
81 | public func encode(to encoder: Encoder) throws {
82 | var container = encoder.container(keyedBy: CodingKeys.self)
83 |
84 |
85 | try container.encode(position.descriptionString(), forKey: .position)
86 |
87 | try container.encode(leftTurn, forKey: .leftTurn)
88 |
89 | try container.encode(rightTurn, forKey: .rightTurn)
90 |
91 | try container.encode(press, forKey: .press)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Keycodes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Keycodes.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/20/21.
6 | //
7 |
8 | import Foundation
9 |
10 | //Ported from https://github.com/adafruit/Adafruit_CircuitPython_HID/blob/main/adafruit_hid/keycode.py
11 |
12 | enum AdafruitPythonHIDKeycode: String, CaseIterable, Identifiable {
13 |
14 | var id: String {
15 | return rawValue
16 | }
17 |
18 | case
19 | COMMAND = "0xE3",
20 | OPTION = "0xE2",
21 | CONTROL = "0xE0",
22 | SHIFT = "0xE1",
23 | ENTER = "0x28",
24 | ESCAPE = "0x29",
25 | BACKSPACE = "0x2A",
26 | TAB = "0x2B",
27 | CAPS_LOCK = "0x39",
28 | PRINT_SCREEN = "0x46",
29 | SCROLL_LOCK = "0x47",
30 | PAUSE = "0x48",
31 | INSERT = "0x49",
32 | HOME = "0x4A",
33 | PAGE_UP = "0x4B",
34 | DELETE = "0x4C",
35 | END = "0x4D",
36 | PAGE_DOWN = "0x4E",
37 | RIGHT_ARROW = "0x4F",
38 | LEFT_ARROW = "0x50",
39 | DOWN_ARROW = "0x51",
40 | UP_ARROW = "0x52",
41 | KP_NUMLOCK = "0x53",
42 | KP_FWD_SLASH = "0x54",
43 | KP_ASTERISK = "0x55",
44 | KP_MINUS = "0x56",
45 | KP_PLUS = "0x57",
46 | KP_ENTER = "0x58",
47 | KP_ONE = "0x59",
48 | KP_TWO = "0x5A",
49 | KP_THREE = "0x5B",
50 | KP_FOUR = "0x5C",
51 | KP_FIVE = "0x5D",
52 | KP_SIX = "0x5E",
53 | KP_SEVEN = "0x5F",
54 | KP_EIGHT = "0x60",
55 | KP_NINE = "0x61",
56 | KP_ZERO = "0x62",
57 | KP_PERIOD = "0x63",
58 | KP_BACKSLASH = "0x64",
59 | APPLICATION = "0x65",
60 | POWER = "0x66",
61 | KP_EQUALS = "0x67",
62 | F1 = "0x3A",
63 | F2 = "0x3B",
64 | F3 = "0x3C",
65 | F4 = "0x3D",
66 | F5 = "0x3E",
67 | F6 = "0x3F",
68 | F7 = "0x40",
69 | F8 = "0x41",
70 | F9 = "0x42",
71 | F10 = "0x43",
72 | F11 = "0x44",
73 | F12 = "0x45",
74 | F13 = "0x68",
75 | F14 = "0x69",
76 | F15 = "0x6A",
77 | F16 = "0x6B",
78 | F17 = "0x6C",
79 | F18 = "0x6D",
80 | F19 = "0x6E",
81 | F20 = "0x6F",
82 | F21 = "0x70",
83 | F22 = "0x71",
84 | F23 = "0x72",
85 | F24 = "0x73",
86 | RIGHT_CTRL = "0xE4",
87 | RIGHT_SHIFT = "0xE5",
88 | RIGHT_ALT = "0xE6",
89 | RIGHT_CMD = "0xE7"
90 |
91 | }
92 |
93 | enum AdafruitHIDPythonMediaControlCode: String, CaseIterable, Identifiable {
94 |
95 | var id: String {
96 | return rawValue
97 | }
98 |
99 | case
100 | RECORD = "0xB2",
101 | FAST_FWD = "0xB3",
102 | REWIND = "0xB4",
103 | SCAN_NEXT = "0xB5",
104 | SCAN_PREV = "0xB6",
105 | STOP = "0xB7",
106 | EJECT = "0xB8",
107 | PLAY_PAUSE = "0xCD",
108 | MUTE = "0xE2",
109 | VOLUME_UP = "0xEA",
110 | VOLUME_DOWN = "0xE9",
111 | BRIGHT_UP = "0x70",
112 | BRIGHT_DOWN = "0x6F"
113 | }
114 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Utility/ReorderableForEach.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReorderableForEach.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/20/21.
6 | //
7 |
8 | import SwiftUI
9 | import UniformTypeIdentifiers
10 |
11 |
12 | //via: https://stackoverflow.com/a/68963988/150181
13 |
14 | struct ReorderableForEach: View {
15 | let items: [Item]
16 | let content: (Item) -> Content
17 | let moveAction: (IndexSet, Int) -> Void
18 |
19 | // A little hack that is needed in order to make view back opaque
20 | // if the drag and drop hasn't ever changed the position
21 | // Without this hack the item remains semi-transparent
22 | @State private var hasChangedLocation: Bool = false
23 |
24 | init(
25 | items: [Item],
26 | @ViewBuilder content: @escaping (Item) -> Content,
27 | moveAction: @escaping (IndexSet, Int) -> Void
28 | ) {
29 | self.items = items
30 | self.content = content
31 | self.moveAction = moveAction
32 | }
33 |
34 | @State private var draggingItem: Item?
35 |
36 | var body: some View {
37 | ForEach(items) { item in
38 | content(item)
39 | .overlay(draggingItem == item && hasChangedLocation ? Color.white.opacity(0.8) : Color.clear)
40 | .onDrag {
41 | draggingItem = item
42 | return NSItemProvider(object: "\(item.id)" as NSString)
43 | }
44 | .onDrop(
45 | of: [UTType.text],
46 | delegate: DragRelocateDelegate(
47 | item: item,
48 | listData: items,
49 | current: $draggingItem,
50 | hasChangedLocation: $hasChangedLocation
51 | ) { from, to in
52 | withAnimation {
53 | moveAction(from, to)
54 | }
55 | }
56 | )
57 | }
58 | }
59 | }
60 |
61 | struct DragRelocateDelegate: DropDelegate {
62 | let item: Item
63 | var listData: [Item]
64 | @Binding var current: Item?
65 | @Binding var hasChangedLocation: Bool
66 |
67 | var moveAction: (IndexSet, Int) -> Void
68 |
69 | func dropEntered(info: DropInfo) {
70 | guard item != current, let current = current else { return }
71 | guard let from = listData.firstIndex(of: current), let to = listData.firstIndex(of: item) else { return }
72 |
73 | hasChangedLocation = true
74 |
75 | if listData[to] != current {
76 | moveAction(IndexSet(integer: from), to > from ? to + 1 : to)
77 | }
78 | }
79 |
80 | func dropUpdated(info: DropInfo) -> DropProposal? {
81 | DropProposal(operation: .move)
82 | }
83 |
84 | func performDrop(info: DropInfo) -> Bool {
85 | hasChangedLocation = false
86 | current = nil
87 | return true
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Macropad Toolbox/ConfigurationListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigurationListView.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ConfigurationListView: View {
11 |
12 | @Environment(\.managedObjectContext) private var viewContext
13 |
14 | @FetchRequest(
15 | sortDescriptors: [NSSortDescriptor(keyPath: \Configuration.modificationDate, ascending: true)],
16 | animation: .default)
17 | private var items: FetchedResults
18 |
19 | var body: some View {
20 | List {
21 | ForEach(items) { item in
22 | NavigationLink {
23 | PageListView(configuration: item)
24 | } label: {
25 | VStack(alignment: .leading) {
26 | Text(item.name ?? "")
27 | Text(item.modificationDate!, formatter: itemFormatter)
28 | .foregroundColor(.secondary)
29 | }
30 |
31 |
32 | }
33 | }
34 | .onDelete(perform: deleteItems)
35 | }
36 | .toolbar {
37 | ToolbarItem {
38 | Button(action: addItem) {
39 | Label("Add Item", systemImage: "plus")
40 | }
41 | }
42 | }
43 | }
44 |
45 | private func addItem() {
46 | withAnimation {
47 | let newItem = Configuration(context: viewContext)
48 | newItem.modificationDate = Date()
49 |
50 | do {
51 | try viewContext.save()
52 | } catch {
53 | // Replace this implementation with code to handle the error appropriately.
54 | // 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.
55 | let nsError = error as NSError
56 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
57 | }
58 | }
59 | }
60 |
61 | private func deleteItems(offsets: IndexSet) {
62 | withAnimation {
63 | offsets.map { items[$0] }.forEach(viewContext.delete)
64 |
65 | do {
66 | try viewContext.save()
67 | } catch {
68 | // Replace this implementation with code to handle the error appropriately.
69 | // 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.
70 | let nsError = error as NSError
71 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
72 | }
73 | }
74 | }
75 | }
76 |
77 | private let itemFormatter: DateFormatter = {
78 | let formatter = DateFormatter()
79 | formatter.dateStyle = .short
80 | formatter.timeStyle = .medium
81 | return formatter
82 | }()
83 |
84 | struct ConfigurationListView_Previews: PreviewProvider {
85 | static var previews: some View {
86 | ConfigurationListView()
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Utility/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import Foundation
9 |
10 | import SwiftUI
11 | extension Encodable {
12 |
13 | public func toJSONString() -> String {
14 | let jsonData = try! JSONEncoder().encode(self)
15 | return String(data: jsonData, encoding: .utf8)!
16 | }
17 |
18 | }
19 |
20 | func instantiate(jsonString: String) -> T? {
21 | return try? JSONDecoder().decode(T.self, from: jsonString.data(using: .utf8)!)
22 | }
23 |
24 | extension Color: Codable {
25 | init(hex: String) {
26 | let rgba = hex.toRGBA()
27 |
28 | self.init(.sRGB,
29 | red: Double(rgba.r),
30 | green: Double(rgba.g),
31 | blue: Double(rgba.b),
32 | opacity: Double(rgba.alpha))
33 | }
34 |
35 | public init(from decoder: Decoder) throws {
36 | let container = try decoder.singleValueContainer()
37 | let hex = try container.decode(String.self)
38 |
39 | self.init(hex: hex)
40 | }
41 |
42 | public func encode(to encoder: Encoder) throws {
43 | var container = encoder.singleValueContainer()
44 | try container.encode(toHex)
45 | }
46 |
47 | var toHex: String? {
48 | return toHex()
49 | }
50 |
51 | func toHex(alpha: Bool = false) -> String? {
52 | guard let components = cgColor?.components, components.count >= 3 else {
53 | return nil
54 | }
55 |
56 | let r = Float(components[0])
57 | let g = Float(components[1])
58 | let b = Float(components[2])
59 | var a = Float(1.0)
60 |
61 | if components.count >= 4 {
62 | a = Float(components[3])
63 | }
64 |
65 | if alpha {
66 | return String(format: "%02lX%02lX%02lX%02lX",
67 | lroundf(r * 255),
68 | lroundf(g * 255),
69 | lroundf(b * 255),
70 | lroundf(a * 255))
71 | }
72 | else {
73 | return String(format: "%02lX%02lX%02lX",
74 | lroundf(r * 255),
75 | lroundf(g * 255),
76 | lroundf(b * 255))
77 | }
78 | }
79 | }
80 |
81 | extension String {
82 | func toRGBA() -> (r: CGFloat, g: CGFloat, b: CGFloat, alpha: CGFloat) {
83 | var hexSanitized = self.trimmingCharacters(in: .whitespacesAndNewlines)
84 | hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
85 |
86 | var rgb: UInt64 = 0
87 |
88 | var r: CGFloat = 0.0
89 | var g: CGFloat = 0.0
90 | var b: CGFloat = 0.0
91 | var a: CGFloat = 1.0
92 |
93 | let length = hexSanitized.count
94 |
95 | Scanner(string: hexSanitized).scanHexInt64(&rgb)
96 |
97 | if length == 6 {
98 | r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
99 | g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
100 | b = CGFloat(rgb & 0x0000FF) / 255.0
101 | }
102 | else if length == 8 {
103 | r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
104 | g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
105 | b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
106 | a = CGFloat(rgb & 0x000000FF) / 255.0
107 | }
108 |
109 | return (r, g, b, a)
110 | }
111 | }
112 |
113 | func ??(lhs: Binding>, rhs: T) -> Binding {
114 | //via: https://stackoverflow.com/a/61002589/150181
115 | Binding(
116 | get: { lhs.wrappedValue ?? rhs },
117 | set: { lhs.wrappedValue = $0 }
118 | )
119 | }
120 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Core Data/Macropad_Toolbox.xcdatamodeld/Macropad_Toolbox.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Core Data/Page.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Page+CoreDataClass.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | @objc(Page)
13 | public class Page: NSManagedObject, Codable {
14 |
15 | let nullInvocation = [0,0,0,0]
16 |
17 | var invocationClaimTimer: Timer?
18 |
19 | public override func awakeFromInsert() {
20 | super.awakeFromInsert()
21 |
22 | self.name = "New Page"
23 |
24 | for _ in 1...12 {
25 | let key = Key(context: self.managedObjectContext!)
26 |
27 | self.addToKeys(key)
28 | }
29 |
30 | self.clearInvocation()
31 | }
32 |
33 | public override func awakeFromFetch() {
34 | super.awakeFromFetch()
35 |
36 | if self.invocation == nil || self.invocation?.count == 0 {
37 | self.clearInvocation()
38 |
39 | }
40 | }
41 |
42 | func clearInvocation() {
43 | self.invocation = nullInvocation
44 | }
45 |
46 |
47 | func rotary(for position: RotaryEncoder.Position) -> RotaryEncoder {
48 |
49 | for storedEncoder in self.encoders?.allObjects as! [RotaryEncoder] {
50 | if storedEncoder.position == position {
51 | return storedEncoder
52 | }
53 | }
54 |
55 | let encoder = RotaryEncoder(context: self.managedObjectContext!)
56 | encoder.position = position
57 |
58 | self.addToEncoders(encoder)
59 |
60 | return encoder
61 | }
62 |
63 | func pendingInvocationChange() {
64 |
65 | self.objectWillChange.send()
66 |
67 | self.invocationClaimTimer?.invalidate()
68 | self.invocationClaimTimer = .scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(claimInvocation), userInfo: nil, repeats: false)
69 |
70 | }
71 |
72 | @objc func claimInvocation() {
73 | self.configuration?.claim(invocation: self.invocation!, page: self)
74 | }
75 |
76 | //MARK: - Codable
77 |
78 | enum CodingKeys: CodingKey {
79 | case name,
80 | keys,
81 | invocation,
82 | rotaryEncoders
83 | }
84 |
85 | public required convenience init(from decoder: Decoder) throws {
86 | guard let context = decoder.managedObjectContext else {
87 | throw DecoderConfigurationError.missingManagedObjectContext
88 | }
89 |
90 | self.init(context: context)
91 |
92 | let container = try decoder.container(keyedBy: CodingKeys.self)
93 |
94 | self.name = try container.decode(String.self, forKey: .name)
95 |
96 | let keysArray = try container.decode([Key].self, forKey: .keys)
97 |
98 | self.keys = NSOrderedSet(array: keysArray)
99 |
100 | self.invocation = try container.decode([Int].self, forKey: .invocation)
101 | }
102 |
103 | public func encode(to encoder: Encoder) throws {
104 | var container = encoder.container(keyedBy: CodingKeys.self)
105 |
106 | try container.encodeIfPresent(name, forKey: .name)
107 | try container.encodeIfPresent(invocation, forKey: .invocation)
108 |
109 | let encodersArray = [self.rotary(for: .left), self.rotary(for: .right)]
110 | try container.encode(encodersArray, forKey: .rotaryEncoders)
111 |
112 | if let keysArray = keys?.array as? [Key] {
113 | try container.encode(keysArray, forKey: .keys)
114 | }
115 | }
116 |
117 | func moveKeys(indices: IndexSet, destination: Int) {
118 | var keysArray = keys?.array ?? []
119 |
120 | keysArray.move(fromOffsets: indices, toOffset: destination)
121 |
122 | self.keys = NSOrderedSet(array: keysArray)
123 |
124 | self.configuration?.logUpdate()
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/Macropad Toolbox/RotaryConfigView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RotaryConfigView.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/22/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RotaryConfigView: View {
11 |
12 | @State var activeEvent: RotaryEvent?
13 | @ObservedObject var encoder: RotaryEncoder
14 | @Binding var selectedMacro: Macro?
15 | @Binding var activeEventString: String?
16 | let label: String
17 |
18 | enum RotaryEvent: CaseIterable {
19 | case leftTurn,
20 | press,
21 | rightTurn
22 |
23 | func image() -> Image {
24 | switch self {
25 | case .leftTurn:
26 | return Image(systemName: "chevron.left")
27 | case .rightTurn:
28 | return Image(systemName: "chevron.right")
29 | case .press:
30 | return Image(systemName: "chevron.down")
31 | }
32 | }
33 |
34 | func labelText() -> Text {
35 | switch self {
36 | case .press:
37 | return Text("Press")
38 | default:
39 | return Text("Turn")
40 | }
41 | }
42 |
43 | func groupBoxString() -> String {
44 | switch self {
45 | case .leftTurn:
46 | return "Left Turn"
47 | case .rightTurn:
48 | return "Right Turn"
49 | case .press:
50 | return "Press"
51 | }
52 | }
53 | }
54 |
55 | func handleSelection(for event: RotaryEvent) {
56 |
57 | switch event {
58 | case .leftTurn:
59 | selectedMacro = encoder.leftTurn
60 | case .rightTurn:
61 | selectedMacro = encoder.rightTurn
62 | case .press:
63 | selectedMacro = encoder.press
64 | }
65 | }
66 |
67 | func label(for event: RotaryEvent) -> some View {
68 | return HStack {
69 | event.image()
70 | event.labelText()
71 | }
72 | }
73 |
74 | var body: some View {
75 | VStack {
76 |
77 | Text(label)
78 | .foregroundColor(.secondary)
79 |
80 | ForEach(RotaryEvent.allCases, id: \.self) { event in
81 | ZStack {
82 |
83 | if activeEvent == event {
84 | Color.blue
85 | } else {
86 | Color.specialKeyBackground
87 | }
88 |
89 | label(for: event)
90 | .frame(width: 80, height: 50)
91 |
92 |
93 | }
94 | .frame(height: 25)
95 | .clipped()
96 | .onTapGesture {
97 | self.handleSelection(for: event)
98 | }
99 |
100 | }
101 |
102 |
103 | }
104 | .onChange(of: selectedMacro, perform: { macro in
105 | if macro == encoder.leftTurn {
106 | activeEvent = .leftTurn
107 | } else if macro == encoder.rightTurn {
108 | activeEvent = .rightTurn
109 | } else if macro == encoder.press {
110 | activeEvent = .press
111 | } else {
112 | activeEvent = nil
113 | }
114 |
115 | if let groupBoxString = activeEvent?.groupBoxString(){
116 | activeEventString = "\(groupBoxString), \(encoder.position.descriptionString())"
117 | }
118 |
119 | })
120 | .frame(width: 80)
121 | }
122 | }
123 |
124 | struct RotaryConfigView_Previews: PreviewProvider {
125 | static var previews: some View {
126 | RotaryConfigView(encoder: RotaryEncoder(context: PersistenceController.preview.container.viewContext), selectedMacro: .constant(nil), activeEventString: .constant(nil), label: "Left Encoder")
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/Macropad Toolbox/Core Data/Macro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Macro+CoreDataClass.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | @objc(Macro)
13 | public class Macro: NSManagedObject, Codable {
14 |
15 |
16 | //MARK: - Codable
17 |
18 | enum CodingKeys: CodingKey {
19 | case textContent,
20 | modifiers,
21 | consumerControls
22 | }
23 |
24 | enum SpecialKeyInput: Identifiable, Hashable {
25 | case modifier(AdafruitPythonHIDKeycode),
26 | consumerControl(AdafruitHIDPythonMediaControlCode)
27 |
28 | var id: String {
29 | switch self {
30 | case .modifier(let keycode):
31 | return "modifier:\(keycode.rawValue)"
32 | case .consumerControl(let controlCode):
33 | return "consumerControl:\(controlCode.rawValue)"
34 | }
35 | }
36 |
37 | var uiLabel: String {
38 | switch self {
39 | case .modifier(let keycode):
40 | return String("\(keycode)")
41 | case .consumerControl(let controlCode):
42 | return String("\(controlCode)")
43 | }
44 | }
45 | }
46 |
47 | var preview: String {
48 |
49 | let separator = " + "
50 |
51 | let modifierStrings: [String] = specialKeys.map { String("\($0.uiLabel)") }
52 | let modifierSequence = modifierStrings.joined(separator: separator)
53 |
54 | guard let textContent = textContent, textContent.count > 0 else {
55 | return modifierSequence
56 | }
57 |
58 | if modifierSequence.count > 0 {
59 | return modifierSequence + separator + textContent
60 | }
61 |
62 | if textContent.count > 0 {
63 | return textContent
64 | }
65 |
66 | return ""
67 |
68 | }
69 |
70 | var specialKeys: [SpecialKeyInput] {
71 | get {
72 |
73 | if modifiers == nil {
74 | self.modifiers = []
75 | }
76 |
77 | if consumerControls == nil {
78 | self.consumerControls = []
79 | }
80 |
81 | let selectedModifiers = modifiers!.compactMap { AdafruitPythonHIDKeycode(rawValue: $0)}
82 | let selectedConsumerControls = consumerControls!.compactMap { AdafruitHIDPythonMediaControlCode(rawValue: $0)}
83 |
84 | let wrappedModifiers = selectedModifiers.map { SpecialKeyInput.modifier($0) }
85 | let wrappedConsumercontrols = selectedConsumerControls.map { SpecialKeyInput.consumerControl($0)}
86 |
87 | return wrappedModifiers + wrappedConsumercontrols
88 | }
89 |
90 | set {
91 |
92 | var rawModifiers: [String] = []
93 | var rawConsumerControls: [String] = []
94 |
95 | newValue.forEach { keyInput in
96 |
97 | switch keyInput {
98 | case .modifier(let keycode):
99 | rawModifiers.append(keycode.rawValue)
100 | case .consumerControl(let keycode):
101 | rawConsumerControls.append(keycode.rawValue)
102 | }
103 | }
104 |
105 | self.modifiers = rawModifiers
106 | self.consumerControls = rawConsumerControls
107 |
108 | self.key?.page?.configuration?.logUpdate()
109 | }
110 | }
111 |
112 | func toggleKey(_ key: SpecialKeyInput) {
113 |
114 | if specialKeys.contains(key) {
115 | specialKeys.removeAll { $0 == key }
116 | } else {
117 | specialKeys.append(key)
118 | }
119 | }
120 |
121 | public required convenience init(from decoder: Decoder) throws {
122 | guard let context = decoder.managedObjectContext else {
123 | throw DecoderConfigurationError.missingManagedObjectContext
124 |
125 | }
126 |
127 | self.init(context: context)
128 |
129 | let container = try decoder.container(keyedBy: CodingKeys.self)
130 |
131 | self.textContent = try container.decode(String.self, forKey: .textContent)
132 | self.modifiers = try container.decode([String].self, forKey: .modifiers)
133 | self.consumerControls = try container.decode([String].self, forKey: .consumerControls)
134 | }
135 |
136 | public func encode(to encoder: Encoder) throws {
137 | var container = encoder.container(keyedBy: CodingKeys.self)
138 |
139 | try container.encodeIfPresent(textContent, forKey: .textContent)
140 | try container.encodeIfPresent(modifiers, forKey: .modifiers)
141 | try container.encodeIfPresent(consumerControls, forKey: .consumerControls)
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/Macropad Toolbox/PageDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageDetailView.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PageDetailView: View {
11 |
12 | @Environment(\.managedObjectContext) private var viewContext
13 | @Environment(\.undoManager) var undoManager
14 |
15 | @ObservedObject var page: Page
16 | @State var selectedKey: Key?
17 | @State var selectedRotaryMacro: Macro?
18 | @State var groupBoxLabel: String?
19 | @State var confirmDeleteShown = false
20 |
21 | var keys: [Key] {
22 | guard let keySet = page.keys else {
23 | return []
24 | }
25 |
26 | return keySet.array as! [Key]
27 | }
28 |
29 | var items: [GridItem] {
30 | Array(repeating: .init(.fixed(80)), count: 3)
31 | }
32 |
33 | var body: some View {
34 |
35 | HStack {
36 |
37 | VStack {
38 |
39 | GroupBox("Page Detail") {
40 |
41 | VStack {
42 |
43 | HStack {
44 | Text("Title:")
45 | TextField("Page title:", text: $page.name ?? "")
46 | }
47 |
48 | PageInvocationEditView(page: page)
49 |
50 | Divider()
51 |
52 | HStack {
53 | RotaryConfigView(encoder: page.rotary(for: .left), selectedMacro: $selectedRotaryMacro, activeEventString: $groupBoxLabel, label: "Left Rotary")
54 |
55 | Spacer()
56 |
57 | RotaryConfigView(encoder: page.rotary(for: .right), selectedMacro: $selectedRotaryMacro, activeEventString: $groupBoxLabel, label: "Right Rotary")
58 | }
59 | .padding(.vertical)
60 | .padding(.horizontal, 2)
61 |
62 | LazyVGrid(columns: items) {
63 |
64 | ReorderableForEach(items: keys) { key in
65 |
66 | KeyGridElementView(key: key, selectedKey: $selectedKey)
67 |
68 | } moveAction: { indices, index in
69 | page.moveKeys(indices: indices, destination: index)
70 | }
71 | }
72 |
73 | }
74 | .frame(width: 260)
75 | .padding()
76 |
77 | }
78 | Spacer()
79 |
80 | Button {
81 | confirmDeleteShown = true
82 | } label: {
83 | Label("Delete \(page.name ?? "Unnamed Page")", systemImage: "trash")
84 | }
85 | .confirmationDialog("Are you sure you want to delete \(page.name ?? "")?", isPresented: $confirmDeleteShown, actions: {
86 |
87 |
88 | Button("Delete \(page.name ?? "")", role: .destructive) {
89 | page.configuration?.deletePage(page)
90 | }
91 | Button("Cancel", role: .cancel) {
92 |
93 | }
94 | })
95 | .buttonStyle(.borderless)
96 | .padding(.vertical, 8)
97 |
98 |
99 |
100 |
101 | }
102 |
103 | if let key = selectedKey {
104 | GroupBox("Key details") {
105 |
106 | KeyDetailView(key: key)
107 | .padding(.horizontal)
108 |
109 |
110 | }
111 | }
112 |
113 | if let macro = selectedRotaryMacro, let groupBoxLabel = groupBoxLabel {
114 | GroupBox(groupBoxLabel) {
115 | MacroEditView(macro: macro)
116 | .padding(.horizontal)
117 | }
118 | }
119 |
120 |
121 | Spacer()
122 |
123 |
124 | }
125 | .padding()
126 |
127 | .onAppear {
128 | selectedKey = keys.first
129 | viewContext.undoManager = undoManager
130 | }
131 | .onChange(of: selectedKey) { _ in
132 | if selectedKey != nil {
133 | selectedRotaryMacro = nil
134 | }
135 | }
136 | .onChange(of: selectedRotaryMacro) { _ in
137 | if selectedRotaryMacro != nil {
138 | selectedKey = nil
139 | }
140 | }
141 |
142 |
143 | }
144 | }
145 |
146 |
147 |
148 | struct PageDetailView_Previews: PreviewProvider {
149 | static var previews: some View {
150 | PageDetailView(page: Page(context: PersistenceController.preview.container.viewContext))
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/Macropad Toolbox/PageListView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageListView.swift
3 | // Macropad Toolbox
4 | //
5 | // Created by Danilo Campos on 12/18/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PageListItem: View {
11 |
12 | @ObservedObject var page: Page
13 |
14 | var body: some View {
15 |
16 | HStack {
17 | VStack {
18 | ForEach((0...page.invocation!.count - 1), id: \.self) { index in
19 |
20 | Group {
21 |
22 | if page.invocation![index] == 0 {
23 | Image(systemName: "square")
24 | .foregroundColor(.secondary)
25 | } else {
26 | Image(systemName: "square.fill")
27 | }
28 | }
29 | .font(.system(size: 6))
30 | }
31 | }
32 | .frame(height: 20)
33 |
34 | Text(page.name ?? "")
35 | }
36 | .padding(.vertical, 2)
37 | }
38 | }
39 |
40 | struct PageListView: View {
41 |
42 | @Environment(\.managedObjectContext) private var viewContext
43 | @ObservedObject var configuration: Configuration
44 |
45 | @State var selectedPage: Page?
46 | @State var confirmDeleteShown = false
47 |
48 | var pages: [Page] {
49 | guard let pageSet = configuration.pages else {
50 | return []
51 | }
52 |
53 | return pageSet.array as! [Page]
54 | }
55 |
56 | var body: some View {
57 |
58 | VStack(alignment: .leading, spacing: 0) {
59 |
60 | VStack(alignment: .leading, spacing: 4) {
61 |
62 | TextField("Config Name", text: $configuration.name ?? "", prompt: Text("Config Name"))
63 | .padding([.top, .horizontal])
64 |
65 |
66 | Button(action: {
67 | confirmDeleteShown = true
68 | }) {
69 | Label("Delete", systemImage: "trash")
70 | }
71 | .confirmationDialog("Are you sure you want to delete \(configuration.name ?? "")?", isPresented: $confirmDeleteShown, actions: {
72 |
73 |
74 | Button("Delete \(configuration.name ?? "")", role: .destructive) {
75 | configuration.delete()
76 | }
77 | Button("Cancel", role: .cancel) {
78 |
79 | }
80 | })
81 | .buttonStyle(.borderless)
82 | .padding()
83 | Divider()
84 | .padding([.horizontal, .bottom])
85 |
86 | Text("Pages:")
87 | .frame(maxWidth: .infinity, alignment: .leading)
88 | .padding([.leading, .bottom])
89 | .foregroundColor(.secondary)
90 |
91 | }
92 |
93 | Divider()
94 |
95 |
96 | List{
97 | ForEach(pages) { page in
98 | NavigationLink {
99 | PageDetailView(page: page)
100 | .onAppear {
101 | selectedPage = page
102 | }
103 | } label: {
104 | PageListItem(page: page)
105 | .id(page.invocation)
106 |
107 | }
108 | }
109 | .onDelete(perform: deleteItems)
110 | .onMove { indices, destination in
111 | configuration.movePages(indices: indices, destination: destination)
112 | }
113 | }
114 |
115 | Divider()
116 |
117 | Button(action: addItem) {
118 | Label("Add Page", systemImage: "plus")
119 | }
120 | .buttonStyle(.borderless)
121 | .padding(.horizontal)
122 | .padding(.vertical, 8)
123 |
124 |
125 |
126 | }
127 |
128 | .toolbar {
129 |
130 |
131 | ToolbarItem {
132 | Button {
133 |
134 | viewContext.attemptSaveLoggingErrors()
135 |
136 | do {
137 | let data = try configuration.jsonData()
138 |
139 | let saveURL = showSavePanel()
140 |
141 | write(json: data, to: saveURL)
142 |
143 | } catch {
144 | print("Error generating json: \(error)")
145 | }
146 |
147 |
148 | } label: {
149 | Text("Export \(configuration.name ?? "[Unnamed Configuration]")")
150 | }
151 | }
152 |
153 | }
154 | }
155 |
156 | func showSavePanel() -> URL? {
157 | let savePanel = NSSavePanel()
158 | savePanel.allowedContentTypes = [.json]
159 | savePanel.isExtensionHidden = false
160 | savePanel.allowsOtherFileTypes = false
161 | savePanel.title = "Save your Macropad configuration:"
162 | savePanel.nameFieldLabel = "File name:"
163 | savePanel.nameFieldStringValue = "macro.json"
164 |
165 | let response = savePanel.runModal()
166 | return response == .OK ? savePanel.url : nil
167 | }
168 |
169 | func write(json: Data, to url: URL?) {
170 | guard let url = url else {
171 | return
172 | }
173 |
174 |
175 | do {
176 | try json.write(to: url)
177 | } catch {
178 | print("Error writing to file: \(error)")
179 | }
180 |
181 | }
182 |
183 | private func deleteConfig() {
184 |
185 | }
186 |
187 | private func addItem() {
188 | withAnimation {
189 | let newItem = Page(context: viewContext)
190 |
191 | configuration.addToPages(newItem)
192 | configuration.objectWillChange.send()
193 |
194 | do {
195 | try viewContext.save()
196 | } catch {
197 | // Replace this implementation with code to handle the error appropriately.
198 | // 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.
199 | let nsError = error as NSError
200 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
201 | }
202 | }
203 | }
204 |
205 | private func deleteItems(offsets: IndexSet) {
206 | withAnimation {
207 | offsets.map { configuration.pages?[$0] as! NSManagedObject }.forEach(viewContext.delete)
208 |
209 | do {
210 | try viewContext.save()
211 | } catch {
212 | // Replace this implementation with code to handle the error appropriately.
213 | // 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.
214 | let nsError = error as NSError
215 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
216 | }
217 | }
218 | }
219 | }
220 |
221 | private struct SelectedPageKey: FocusedValueKey {
222 | typealias Value = Binding
223 | }
224 |
225 | extension FocusedValues {
226 | var selectedPage: Binding? {
227 | get { self[SelectedPageKey.self] }
228 | set { self[SelectedPageKey.self] = newValue }
229 | }
230 | }
231 |
232 | struct PageListView_Previews: PreviewProvider {
233 | static var previews: some View {
234 | PageListView(configuration: Configuration(context: PersistenceController.preview.container.viewContext))
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/Macropad Toolbox.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 1F503BB729554B48003DC5DE /* RotaryConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F503BB629554B48003DC5DE /* RotaryConfigView.swift */; };
11 | 1F503BBA29555166003DC5DE /* RotaryEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F503BB829555166003DC5DE /* RotaryEncoder.swift */; };
12 | 1F503BBD29556BAD003DC5DE /* PageInvocationEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F503BBC29556BAD003DC5DE /* PageInvocationEditView.swift */; };
13 | 1FFBF849276E6E3700918BE8 /* Macropad_ToolboxApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF848276E6E3700918BE8 /* Macropad_ToolboxApp.swift */; };
14 | 1FFBF84B276E6E3700918BE8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF84A276E6E3700918BE8 /* ContentView.swift */; };
15 | 1FFBF84D276E6E3800918BE8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1FFBF84C276E6E3800918BE8 /* Assets.xcassets */; };
16 | 1FFBF850276E6E3800918BE8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1FFBF84F276E6E3800918BE8 /* Preview Assets.xcassets */; };
17 | 1FFBF852276E6E3800918BE8 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF851276E6E3800918BE8 /* Persistence.swift */; };
18 | 1FFBF855276E6E3800918BE8 /* Macropad_Toolbox.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF853276E6E3800918BE8 /* Macropad_Toolbox.xcdatamodeld */; };
19 | 1FFBF860276E6E3800918BE8 /* Macropad_ToolboxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF85F276E6E3800918BE8 /* Macropad_ToolboxTests.swift */; };
20 | 1FFBF86A276E6E3800918BE8 /* Macropad_ToolboxUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF869276E6E3800918BE8 /* Macropad_ToolboxUITests.swift */; };
21 | 1FFBF86C276E6E3800918BE8 /* Macropad_ToolboxUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF86B276E6E3800918BE8 /* Macropad_ToolboxUITestsLaunchTests.swift */; };
22 | 1FFBF881276E762C00918BE8 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF879276E762C00918BE8 /* Configuration.swift */; };
23 | 1FFBF883276E762C00918BE8 /* Macro.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF87B276E762C00918BE8 /* Macro.swift */; };
24 | 1FFBF885276E762C00918BE8 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF87D276E762C00918BE8 /* Key.swift */; };
25 | 1FFBF887276E762C00918BE8 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF87F276E762C00918BE8 /* Page.swift */; };
26 | 1FFBF88A276E783C00918BE8 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF889276E783C00918BE8 /* Extensions.swift */; };
27 | 1FFBF88C276E7D3100918BE8 /* ConfigurationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF88B276E7D3100918BE8 /* ConfigurationListView.swift */; };
28 | 1FFBF88E276E7DED00918BE8 /* PageListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF88D276E7DED00918BE8 /* PageListView.swift */; };
29 | 1FFBF890276E833700918BE8 /* PageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF88F276E833700918BE8 /* PageDetailView.swift */; };
30 | 1FFBF892276E99AF00918BE8 /* KeyDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF891276E99AF00918BE8 /* KeyDetailView.swift */; };
31 | 1FFBF894276E9A8600918BE8 /* KeyGridElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF893276E9A8600918BE8 /* KeyGridElementView.swift */; };
32 | 1FFBF896276E9ED800918BE8 /* JSONExportSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF895276E9ED800918BE8 /* JSONExportSupport.swift */; };
33 | 1FFBF89E2771036400918BE8 /* Keycodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF89D2771036400918BE8 /* Keycodes.swift */; };
34 | 1FFBF8A027710B8C00918BE8 /* ModifierSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF89F27710B8C00918BE8 /* ModifierSelectionView.swift */; };
35 | 1FFBF8A42771341600918BE8 /* ReorderableForEach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF8A32771341600918BE8 /* ReorderableForEach.swift */; };
36 | 1FFBF8A62771355700918BE8 /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFBF8A52771355700918BE8 /* UIConstants.swift */; };
37 | /* End PBXBuildFile section */
38 |
39 | /* Begin PBXContainerItemProxy section */
40 | 1FFBF85C276E6E3800918BE8 /* PBXContainerItemProxy */ = {
41 | isa = PBXContainerItemProxy;
42 | containerPortal = 1FFBF83D276E6E3700918BE8 /* Project object */;
43 | proxyType = 1;
44 | remoteGlobalIDString = 1FFBF844276E6E3700918BE8;
45 | remoteInfo = "Macropad Toolbox";
46 | };
47 | 1FFBF866276E6E3800918BE8 /* PBXContainerItemProxy */ = {
48 | isa = PBXContainerItemProxy;
49 | containerPortal = 1FFBF83D276E6E3700918BE8 /* Project object */;
50 | proxyType = 1;
51 | remoteGlobalIDString = 1FFBF844276E6E3700918BE8;
52 | remoteInfo = "Macropad Toolbox";
53 | };
54 | /* End PBXContainerItemProxy section */
55 |
56 | /* Begin PBXFileReference section */
57 | 1F503BB629554B48003DC5DE /* RotaryConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotaryConfigView.swift; sourceTree = ""; };
58 | 1F503BB829555166003DC5DE /* RotaryEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotaryEncoder.swift; sourceTree = ""; };
59 | 1F503BBC29556BAD003DC5DE /* PageInvocationEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInvocationEditView.swift; sourceTree = ""; };
60 | 1FFBF845276E6E3700918BE8 /* Macropad Toolbox.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Macropad Toolbox.app"; sourceTree = BUILT_PRODUCTS_DIR; };
61 | 1FFBF848276E6E3700918BE8 /* Macropad_ToolboxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Macropad_ToolboxApp.swift; sourceTree = ""; };
62 | 1FFBF84A276E6E3700918BE8 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
63 | 1FFBF84C276E6E3800918BE8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
64 | 1FFBF84F276E6E3800918BE8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
65 | 1FFBF851276E6E3800918BE8 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; };
66 | 1FFBF854276E6E3800918BE8 /* Macropad_Toolbox.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Macropad_Toolbox.xcdatamodel; sourceTree = ""; };
67 | 1FFBF856276E6E3800918BE8 /* Macropad_Toolbox.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Macropad_Toolbox.entitlements; sourceTree = ""; };
68 | 1FFBF85B276E6E3800918BE8 /* Macropad ToolboxTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Macropad ToolboxTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
69 | 1FFBF85F276E6E3800918BE8 /* Macropad_ToolboxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Macropad_ToolboxTests.swift; sourceTree = ""; };
70 | 1FFBF865276E6E3800918BE8 /* Macropad ToolboxUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Macropad ToolboxUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
71 | 1FFBF869276E6E3800918BE8 /* Macropad_ToolboxUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Macropad_ToolboxUITests.swift; sourceTree = ""; };
72 | 1FFBF86B276E6E3800918BE8 /* Macropad_ToolboxUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Macropad_ToolboxUITestsLaunchTests.swift; sourceTree = ""; };
73 | 1FFBF879276E762C00918BE8 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; };
74 | 1FFBF87B276E762C00918BE8 /* Macro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Macro.swift; sourceTree = ""; };
75 | 1FFBF87D276E762C00918BE8 /* Key.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Key.swift; sourceTree = ""; };
76 | 1FFBF87F276E762C00918BE8 /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; };
77 | 1FFBF889276E783C00918BE8 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Extensions.swift; path = "Macropad Toolbox/Utility/Extensions.swift"; sourceTree = SOURCE_ROOT; };
78 | 1FFBF88B276E7D3100918BE8 /* ConfigurationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationListView.swift; sourceTree = ""; };
79 | 1FFBF88D276E7DED00918BE8 /* PageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListView.swift; sourceTree = ""; };
80 | 1FFBF88F276E833700918BE8 /* PageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageDetailView.swift; sourceTree = ""; };
81 | 1FFBF891276E99AF00918BE8 /* KeyDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailView.swift; sourceTree = ""; };
82 | 1FFBF893276E9A8600918BE8 /* KeyGridElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyGridElementView.swift; sourceTree = ""; };
83 | 1FFBF895276E9ED800918BE8 /* JSONExportSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONExportSupport.swift; sourceTree = ""; };
84 | 1FFBF89D2771036400918BE8 /* Keycodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keycodes.swift; sourceTree = ""; };
85 | 1FFBF89F27710B8C00918BE8 /* ModifierSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierSelectionView.swift; sourceTree = ""; };
86 | 1FFBF8A32771341600918BE8 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = ""; };
87 | 1FFBF8A52771355700918BE8 /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; };
88 | /* End PBXFileReference section */
89 |
90 | /* Begin PBXFrameworksBuildPhase section */
91 | 1FFBF842276E6E3700918BE8 /* Frameworks */ = {
92 | isa = PBXFrameworksBuildPhase;
93 | buildActionMask = 2147483647;
94 | files = (
95 | );
96 | runOnlyForDeploymentPostprocessing = 0;
97 | };
98 | 1FFBF858276E6E3800918BE8 /* Frameworks */ = {
99 | isa = PBXFrameworksBuildPhase;
100 | buildActionMask = 2147483647;
101 | files = (
102 | );
103 | runOnlyForDeploymentPostprocessing = 0;
104 | };
105 | 1FFBF862276E6E3800918BE8 /* Frameworks */ = {
106 | isa = PBXFrameworksBuildPhase;
107 | buildActionMask = 2147483647;
108 | files = (
109 | );
110 | runOnlyForDeploymentPostprocessing = 0;
111 | };
112 | /* End PBXFrameworksBuildPhase section */
113 |
114 | /* Begin PBXGroup section */
115 | 1FFBF83C276E6E3700918BE8 = {
116 | isa = PBXGroup;
117 | children = (
118 | 1FFBF847276E6E3700918BE8 /* Macropad Toolbox */,
119 | 1FFBF85E276E6E3800918BE8 /* Macropad ToolboxTests */,
120 | 1FFBF868276E6E3800918BE8 /* Macropad ToolboxUITests */,
121 | 1FFBF846276E6E3700918BE8 /* Products */,
122 | );
123 | sourceTree = "";
124 | };
125 | 1FFBF846276E6E3700918BE8 /* Products */ = {
126 | isa = PBXGroup;
127 | children = (
128 | 1FFBF845276E6E3700918BE8 /* Macropad Toolbox.app */,
129 | 1FFBF85B276E6E3800918BE8 /* Macropad ToolboxTests.xctest */,
130 | 1FFBF865276E6E3800918BE8 /* Macropad ToolboxUITests.xctest */,
131 | );
132 | name = Products;
133 | sourceTree = "";
134 | };
135 | 1FFBF847276E6E3700918BE8 /* Macropad Toolbox */ = {
136 | isa = PBXGroup;
137 | children = (
138 | 1FFBF856276E6E3800918BE8 /* Macropad_Toolbox.entitlements */,
139 | 1FFBF848276E6E3700918BE8 /* Macropad_ToolboxApp.swift */,
140 | 1FFBF84A276E6E3700918BE8 /* ContentView.swift */,
141 | 1FFBF88B276E7D3100918BE8 /* ConfigurationListView.swift */,
142 | 1FFBF88D276E7DED00918BE8 /* PageListView.swift */,
143 | 1FFBF88F276E833700918BE8 /* PageDetailView.swift */,
144 | 1F503BB629554B48003DC5DE /* RotaryConfigView.swift */,
145 | 1FFBF891276E99AF00918BE8 /* KeyDetailView.swift */,
146 | 1FFBF89F27710B8C00918BE8 /* ModifierSelectionView.swift */,
147 | 1FFBF893276E9A8600918BE8 /* KeyGridElementView.swift */,
148 | 1FFBF89D2771036400918BE8 /* Keycodes.swift */,
149 | 1F503BBC29556BAD003DC5DE /* PageInvocationEditView.swift */,
150 | 1FFBF84C276E6E3800918BE8 /* Assets.xcassets */,
151 | 1FFBF8A1277133F700918BE8 /* Utility */,
152 | 1FFBF878276E6FF600918BE8 /* Core Data */,
153 | 1FFBF84E276E6E3800918BE8 /* Preview Content */,
154 | );
155 | path = "Macropad Toolbox";
156 | sourceTree = "";
157 | };
158 | 1FFBF84E276E6E3800918BE8 /* Preview Content */ = {
159 | isa = PBXGroup;
160 | children = (
161 | 1FFBF84F276E6E3800918BE8 /* Preview Assets.xcassets */,
162 | );
163 | path = "Preview Content";
164 | sourceTree = "";
165 | };
166 | 1FFBF85E276E6E3800918BE8 /* Macropad ToolboxTests */ = {
167 | isa = PBXGroup;
168 | children = (
169 | 1FFBF85F276E6E3800918BE8 /* Macropad_ToolboxTests.swift */,
170 | );
171 | path = "Macropad ToolboxTests";
172 | sourceTree = "";
173 | };
174 | 1FFBF868276E6E3800918BE8 /* Macropad ToolboxUITests */ = {
175 | isa = PBXGroup;
176 | children = (
177 | 1FFBF869276E6E3800918BE8 /* Macropad_ToolboxUITests.swift */,
178 | 1FFBF86B276E6E3800918BE8 /* Macropad_ToolboxUITestsLaunchTests.swift */,
179 | );
180 | path = "Macropad ToolboxUITests";
181 | sourceTree = "";
182 | };
183 | 1FFBF878276E6FF600918BE8 /* Core Data */ = {
184 | isa = PBXGroup;
185 | children = (
186 | 1FFBF851276E6E3800918BE8 /* Persistence.swift */,
187 | 1FFBF853276E6E3800918BE8 /* Macropad_Toolbox.xcdatamodeld */,
188 | 1FFBF879276E762C00918BE8 /* Configuration.swift */,
189 | 1FFBF87B276E762C00918BE8 /* Macro.swift */,
190 | 1FFBF87D276E762C00918BE8 /* Key.swift */,
191 | 1FFBF87F276E762C00918BE8 /* Page.swift */,
192 | 1FFBF895276E9ED800918BE8 /* JSONExportSupport.swift */,
193 | 1F503BB829555166003DC5DE /* RotaryEncoder.swift */,
194 | );
195 | path = "Core Data";
196 | sourceTree = "";
197 | };
198 | 1FFBF8A1277133F700918BE8 /* Utility */ = {
199 | isa = PBXGroup;
200 | children = (
201 | 1FFBF889276E783C00918BE8 /* Extensions.swift */,
202 | 1FFBF8A32771341600918BE8 /* ReorderableForEach.swift */,
203 | 1FFBF8A52771355700918BE8 /* UIConstants.swift */,
204 | );
205 | path = Utility;
206 | sourceTree = "";
207 | };
208 | /* End PBXGroup section */
209 |
210 | /* Begin PBXNativeTarget section */
211 | 1FFBF844276E6E3700918BE8 /* Macropad Toolbox */ = {
212 | isa = PBXNativeTarget;
213 | buildConfigurationList = 1FFBF86F276E6E3800918BE8 /* Build configuration list for PBXNativeTarget "Macropad Toolbox" */;
214 | buildPhases = (
215 | 1FFBF841276E6E3700918BE8 /* Sources */,
216 | 1FFBF842276E6E3700918BE8 /* Frameworks */,
217 | 1FFBF843276E6E3700918BE8 /* Resources */,
218 | );
219 | buildRules = (
220 | );
221 | dependencies = (
222 | );
223 | name = "Macropad Toolbox";
224 | packageProductDependencies = (
225 | );
226 | productName = "Macropad Toolbox";
227 | productReference = 1FFBF845276E6E3700918BE8 /* Macropad Toolbox.app */;
228 | productType = "com.apple.product-type.application";
229 | };
230 | 1FFBF85A276E6E3800918BE8 /* Macropad ToolboxTests */ = {
231 | isa = PBXNativeTarget;
232 | buildConfigurationList = 1FFBF872276E6E3800918BE8 /* Build configuration list for PBXNativeTarget "Macropad ToolboxTests" */;
233 | buildPhases = (
234 | 1FFBF857276E6E3800918BE8 /* Sources */,
235 | 1FFBF858276E6E3800918BE8 /* Frameworks */,
236 | 1FFBF859276E6E3800918BE8 /* Resources */,
237 | );
238 | buildRules = (
239 | );
240 | dependencies = (
241 | 1FFBF85D276E6E3800918BE8 /* PBXTargetDependency */,
242 | );
243 | name = "Macropad ToolboxTests";
244 | productName = "Macropad ToolboxTests";
245 | productReference = 1FFBF85B276E6E3800918BE8 /* Macropad ToolboxTests.xctest */;
246 | productType = "com.apple.product-type.bundle.unit-test";
247 | };
248 | 1FFBF864276E6E3800918BE8 /* Macropad ToolboxUITests */ = {
249 | isa = PBXNativeTarget;
250 | buildConfigurationList = 1FFBF875276E6E3800918BE8 /* Build configuration list for PBXNativeTarget "Macropad ToolboxUITests" */;
251 | buildPhases = (
252 | 1FFBF861276E6E3800918BE8 /* Sources */,
253 | 1FFBF862276E6E3800918BE8 /* Frameworks */,
254 | 1FFBF863276E6E3800918BE8 /* Resources */,
255 | );
256 | buildRules = (
257 | );
258 | dependencies = (
259 | 1FFBF867276E6E3800918BE8 /* PBXTargetDependency */,
260 | );
261 | name = "Macropad ToolboxUITests";
262 | productName = "Macropad ToolboxUITests";
263 | productReference = 1FFBF865276E6E3800918BE8 /* Macropad ToolboxUITests.xctest */;
264 | productType = "com.apple.product-type.bundle.ui-testing";
265 | };
266 | /* End PBXNativeTarget section */
267 |
268 | /* Begin PBXProject section */
269 | 1FFBF83D276E6E3700918BE8 /* Project object */ = {
270 | isa = PBXProject;
271 | attributes = {
272 | BuildIndependentTargetsInParallel = 1;
273 | LastSwiftUpdateCheck = 1310;
274 | LastUpgradeCheck = 1310;
275 | TargetAttributes = {
276 | 1FFBF844276E6E3700918BE8 = {
277 | CreatedOnToolsVersion = 13.1;
278 | };
279 | 1FFBF85A276E6E3800918BE8 = {
280 | CreatedOnToolsVersion = 13.1;
281 | TestTargetID = 1FFBF844276E6E3700918BE8;
282 | };
283 | 1FFBF864276E6E3800918BE8 = {
284 | CreatedOnToolsVersion = 13.1;
285 | TestTargetID = 1FFBF844276E6E3700918BE8;
286 | };
287 | };
288 | };
289 | buildConfigurationList = 1FFBF840276E6E3700918BE8 /* Build configuration list for PBXProject "Macropad Toolbox" */;
290 | compatibilityVersion = "Xcode 13.0";
291 | developmentRegion = en;
292 | hasScannedForEncodings = 0;
293 | knownRegions = (
294 | en,
295 | Base,
296 | );
297 | mainGroup = 1FFBF83C276E6E3700918BE8;
298 | packageReferences = (
299 | );
300 | productRefGroup = 1FFBF846276E6E3700918BE8 /* Products */;
301 | projectDirPath = "";
302 | projectRoot = "";
303 | targets = (
304 | 1FFBF844276E6E3700918BE8 /* Macropad Toolbox */,
305 | 1FFBF85A276E6E3800918BE8 /* Macropad ToolboxTests */,
306 | 1FFBF864276E6E3800918BE8 /* Macropad ToolboxUITests */,
307 | );
308 | };
309 | /* End PBXProject section */
310 |
311 | /* Begin PBXResourcesBuildPhase section */
312 | 1FFBF843276E6E3700918BE8 /* Resources */ = {
313 | isa = PBXResourcesBuildPhase;
314 | buildActionMask = 2147483647;
315 | files = (
316 | 1FFBF850276E6E3800918BE8 /* Preview Assets.xcassets in Resources */,
317 | 1FFBF84D276E6E3800918BE8 /* Assets.xcassets in Resources */,
318 | );
319 | runOnlyForDeploymentPostprocessing = 0;
320 | };
321 | 1FFBF859276E6E3800918BE8 /* Resources */ = {
322 | isa = PBXResourcesBuildPhase;
323 | buildActionMask = 2147483647;
324 | files = (
325 | );
326 | runOnlyForDeploymentPostprocessing = 0;
327 | };
328 | 1FFBF863276E6E3800918BE8 /* Resources */ = {
329 | isa = PBXResourcesBuildPhase;
330 | buildActionMask = 2147483647;
331 | files = (
332 | );
333 | runOnlyForDeploymentPostprocessing = 0;
334 | };
335 | /* End PBXResourcesBuildPhase section */
336 |
337 | /* Begin PBXSourcesBuildPhase section */
338 | 1FFBF841276E6E3700918BE8 /* Sources */ = {
339 | isa = PBXSourcesBuildPhase;
340 | buildActionMask = 2147483647;
341 | files = (
342 | 1FFBF89E2771036400918BE8 /* Keycodes.swift in Sources */,
343 | 1FFBF852276E6E3800918BE8 /* Persistence.swift in Sources */,
344 | 1FFBF8A42771341600918BE8 /* ReorderableForEach.swift in Sources */,
345 | 1FFBF88C276E7D3100918BE8 /* ConfigurationListView.swift in Sources */,
346 | 1FFBF892276E99AF00918BE8 /* KeyDetailView.swift in Sources */,
347 | 1FFBF88A276E783C00918BE8 /* Extensions.swift in Sources */,
348 | 1FFBF881276E762C00918BE8 /* Configuration.swift in Sources */,
349 | 1F503BBD29556BAD003DC5DE /* PageInvocationEditView.swift in Sources */,
350 | 1F503BB729554B48003DC5DE /* RotaryConfigView.swift in Sources */,
351 | 1FFBF883276E762C00918BE8 /* Macro.swift in Sources */,
352 | 1FFBF84B276E6E3700918BE8 /* ContentView.swift in Sources */,
353 | 1FFBF890276E833700918BE8 /* PageDetailView.swift in Sources */,
354 | 1FFBF885276E762C00918BE8 /* Key.swift in Sources */,
355 | 1FFBF88E276E7DED00918BE8 /* PageListView.swift in Sources */,
356 | 1FFBF8A027710B8C00918BE8 /* ModifierSelectionView.swift in Sources */,
357 | 1FFBF8A62771355700918BE8 /* UIConstants.swift in Sources */,
358 | 1F503BBA29555166003DC5DE /* RotaryEncoder.swift in Sources */,
359 | 1FFBF896276E9ED800918BE8 /* JSONExportSupport.swift in Sources */,
360 | 1FFBF855276E6E3800918BE8 /* Macropad_Toolbox.xcdatamodeld in Sources */,
361 | 1FFBF894276E9A8600918BE8 /* KeyGridElementView.swift in Sources */,
362 | 1FFBF887276E762C00918BE8 /* Page.swift in Sources */,
363 | 1FFBF849276E6E3700918BE8 /* Macropad_ToolboxApp.swift in Sources */,
364 | );
365 | runOnlyForDeploymentPostprocessing = 0;
366 | };
367 | 1FFBF857276E6E3800918BE8 /* Sources */ = {
368 | isa = PBXSourcesBuildPhase;
369 | buildActionMask = 2147483647;
370 | files = (
371 | 1FFBF860276E6E3800918BE8 /* Macropad_ToolboxTests.swift in Sources */,
372 | );
373 | runOnlyForDeploymentPostprocessing = 0;
374 | };
375 | 1FFBF861276E6E3800918BE8 /* Sources */ = {
376 | isa = PBXSourcesBuildPhase;
377 | buildActionMask = 2147483647;
378 | files = (
379 | 1FFBF86A276E6E3800918BE8 /* Macropad_ToolboxUITests.swift in Sources */,
380 | 1FFBF86C276E6E3800918BE8 /* Macropad_ToolboxUITestsLaunchTests.swift in Sources */,
381 | );
382 | runOnlyForDeploymentPostprocessing = 0;
383 | };
384 | /* End PBXSourcesBuildPhase section */
385 |
386 | /* Begin PBXTargetDependency section */
387 | 1FFBF85D276E6E3800918BE8 /* PBXTargetDependency */ = {
388 | isa = PBXTargetDependency;
389 | target = 1FFBF844276E6E3700918BE8 /* Macropad Toolbox */;
390 | targetProxy = 1FFBF85C276E6E3800918BE8 /* PBXContainerItemProxy */;
391 | };
392 | 1FFBF867276E6E3800918BE8 /* PBXTargetDependency */ = {
393 | isa = PBXTargetDependency;
394 | target = 1FFBF844276E6E3700918BE8 /* Macropad Toolbox */;
395 | targetProxy = 1FFBF866276E6E3800918BE8 /* PBXContainerItemProxy */;
396 | };
397 | /* End PBXTargetDependency section */
398 |
399 | /* Begin XCBuildConfiguration section */
400 | 1FFBF86D276E6E3800918BE8 /* Debug */ = {
401 | isa = XCBuildConfiguration;
402 | buildSettings = {
403 | ALWAYS_SEARCH_USER_PATHS = NO;
404 | CLANG_ANALYZER_NONNULL = YES;
405 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
406 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
407 | CLANG_CXX_LIBRARY = "libc++";
408 | CLANG_ENABLE_MODULES = YES;
409 | CLANG_ENABLE_OBJC_ARC = YES;
410 | CLANG_ENABLE_OBJC_WEAK = YES;
411 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
412 | CLANG_WARN_BOOL_CONVERSION = YES;
413 | CLANG_WARN_COMMA = YES;
414 | CLANG_WARN_CONSTANT_CONVERSION = YES;
415 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
416 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
417 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
418 | CLANG_WARN_EMPTY_BODY = YES;
419 | CLANG_WARN_ENUM_CONVERSION = YES;
420 | CLANG_WARN_INFINITE_RECURSION = YES;
421 | CLANG_WARN_INT_CONVERSION = YES;
422 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
423 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
424 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
425 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
426 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
427 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
428 | CLANG_WARN_STRICT_PROTOTYPES = YES;
429 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
430 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
431 | CLANG_WARN_UNREACHABLE_CODE = YES;
432 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
433 | COPY_PHASE_STRIP = NO;
434 | DEBUG_INFORMATION_FORMAT = dwarf;
435 | ENABLE_STRICT_OBJC_MSGSEND = YES;
436 | ENABLE_TESTABILITY = YES;
437 | GCC_C_LANGUAGE_STANDARD = gnu11;
438 | GCC_DYNAMIC_NO_PIC = NO;
439 | GCC_NO_COMMON_BLOCKS = YES;
440 | GCC_OPTIMIZATION_LEVEL = 0;
441 | GCC_PREPROCESSOR_DEFINITIONS = (
442 | "DEBUG=1",
443 | "$(inherited)",
444 | );
445 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
446 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
447 | GCC_WARN_UNDECLARED_SELECTOR = YES;
448 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
449 | GCC_WARN_UNUSED_FUNCTION = YES;
450 | GCC_WARN_UNUSED_VARIABLE = YES;
451 | MACOSX_DEPLOYMENT_TARGET = 12.0;
452 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
453 | MTL_FAST_MATH = YES;
454 | ONLY_ACTIVE_ARCH = YES;
455 | SDKROOT = macosx;
456 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
457 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
458 | };
459 | name = Debug;
460 | };
461 | 1FFBF86E276E6E3800918BE8 /* Release */ = {
462 | isa = XCBuildConfiguration;
463 | buildSettings = {
464 | ALWAYS_SEARCH_USER_PATHS = NO;
465 | CLANG_ANALYZER_NONNULL = YES;
466 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
467 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
468 | CLANG_CXX_LIBRARY = "libc++";
469 | CLANG_ENABLE_MODULES = YES;
470 | CLANG_ENABLE_OBJC_ARC = YES;
471 | CLANG_ENABLE_OBJC_WEAK = YES;
472 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
473 | CLANG_WARN_BOOL_CONVERSION = YES;
474 | CLANG_WARN_COMMA = YES;
475 | CLANG_WARN_CONSTANT_CONVERSION = YES;
476 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
477 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
478 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
479 | CLANG_WARN_EMPTY_BODY = YES;
480 | CLANG_WARN_ENUM_CONVERSION = YES;
481 | CLANG_WARN_INFINITE_RECURSION = YES;
482 | CLANG_WARN_INT_CONVERSION = YES;
483 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
484 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
485 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
486 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
487 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
488 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
489 | CLANG_WARN_STRICT_PROTOTYPES = YES;
490 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
491 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
492 | CLANG_WARN_UNREACHABLE_CODE = YES;
493 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
494 | COPY_PHASE_STRIP = NO;
495 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
496 | ENABLE_NS_ASSERTIONS = NO;
497 | ENABLE_STRICT_OBJC_MSGSEND = YES;
498 | GCC_C_LANGUAGE_STANDARD = gnu11;
499 | GCC_NO_COMMON_BLOCKS = YES;
500 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
501 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
502 | GCC_WARN_UNDECLARED_SELECTOR = YES;
503 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
504 | GCC_WARN_UNUSED_FUNCTION = YES;
505 | GCC_WARN_UNUSED_VARIABLE = YES;
506 | MACOSX_DEPLOYMENT_TARGET = 12.0;
507 | MTL_ENABLE_DEBUG_INFO = NO;
508 | MTL_FAST_MATH = YES;
509 | SDKROOT = macosx;
510 | SWIFT_COMPILATION_MODE = wholemodule;
511 | SWIFT_OPTIMIZATION_LEVEL = "-O";
512 | };
513 | name = Release;
514 | };
515 | 1FFBF870276E6E3800918BE8 /* Debug */ = {
516 | isa = XCBuildConfiguration;
517 | buildSettings = {
518 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
519 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
520 | CODE_SIGN_ENTITLEMENTS = "Macropad Toolbox/Macropad_Toolbox.entitlements";
521 | CODE_SIGN_STYLE = Automatic;
522 | COMBINE_HIDPI_IMAGES = YES;
523 | CURRENT_PROJECT_VERSION = 1;
524 | DEVELOPMENT_ASSET_PATHS = "\"Macropad Toolbox/Preview Content\"";
525 | DEVELOPMENT_TEAM = 2BQHEV6DNR;
526 | ENABLE_HARDENED_RUNTIME = YES;
527 | ENABLE_PREVIEWS = YES;
528 | GENERATE_INFOPLIST_FILE = YES;
529 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
530 | LD_RUNPATH_SEARCH_PATHS = (
531 | "$(inherited)",
532 | "@executable_path/../Frameworks",
533 | );
534 | MARKETING_VERSION = 1.0;
535 | PRODUCT_BUNDLE_IDENTIFIER = "danilocampos.Macropad-Toolbox";
536 | PRODUCT_NAME = "$(TARGET_NAME)";
537 | SWIFT_EMIT_LOC_STRINGS = YES;
538 | SWIFT_VERSION = 5.0;
539 | };
540 | name = Debug;
541 | };
542 | 1FFBF871276E6E3800918BE8 /* Release */ = {
543 | isa = XCBuildConfiguration;
544 | buildSettings = {
545 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
546 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
547 | CODE_SIGN_ENTITLEMENTS = "Macropad Toolbox/Macropad_Toolbox.entitlements";
548 | CODE_SIGN_STYLE = Automatic;
549 | COMBINE_HIDPI_IMAGES = YES;
550 | CURRENT_PROJECT_VERSION = 1;
551 | DEVELOPMENT_ASSET_PATHS = "\"Macropad Toolbox/Preview Content\"";
552 | DEVELOPMENT_TEAM = 2BQHEV6DNR;
553 | ENABLE_HARDENED_RUNTIME = YES;
554 | ENABLE_PREVIEWS = YES;
555 | GENERATE_INFOPLIST_FILE = YES;
556 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
557 | LD_RUNPATH_SEARCH_PATHS = (
558 | "$(inherited)",
559 | "@executable_path/../Frameworks",
560 | );
561 | MARKETING_VERSION = 1.0;
562 | PRODUCT_BUNDLE_IDENTIFIER = "danilocampos.Macropad-Toolbox";
563 | PRODUCT_NAME = "$(TARGET_NAME)";
564 | SWIFT_EMIT_LOC_STRINGS = YES;
565 | SWIFT_VERSION = 5.0;
566 | };
567 | name = Release;
568 | };
569 | 1FFBF873276E6E3800918BE8 /* Debug */ = {
570 | isa = XCBuildConfiguration;
571 | buildSettings = {
572 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
573 | BUNDLE_LOADER = "$(TEST_HOST)";
574 | CODE_SIGN_STYLE = Automatic;
575 | COMBINE_HIDPI_IMAGES = YES;
576 | CURRENT_PROJECT_VERSION = 1;
577 | DEVELOPMENT_TEAM = 2BQHEV6DNR;
578 | GENERATE_INFOPLIST_FILE = YES;
579 | LD_RUNPATH_SEARCH_PATHS = (
580 | "$(inherited)",
581 | "@executable_path/../Frameworks",
582 | "@loader_path/../Frameworks",
583 | );
584 | MACOSX_DEPLOYMENT_TARGET = 12.0;
585 | MARKETING_VERSION = 1.0;
586 | PRODUCT_BUNDLE_IDENTIFIER = "danilocampos.Macropad-ToolboxTests";
587 | PRODUCT_NAME = "$(TARGET_NAME)";
588 | SWIFT_EMIT_LOC_STRINGS = NO;
589 | SWIFT_VERSION = 5.0;
590 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Macropad Toolbox.app/Contents/MacOS/Macropad Toolbox";
591 | };
592 | name = Debug;
593 | };
594 | 1FFBF874276E6E3800918BE8 /* Release */ = {
595 | isa = XCBuildConfiguration;
596 | buildSettings = {
597 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
598 | BUNDLE_LOADER = "$(TEST_HOST)";
599 | CODE_SIGN_STYLE = Automatic;
600 | COMBINE_HIDPI_IMAGES = YES;
601 | CURRENT_PROJECT_VERSION = 1;
602 | DEVELOPMENT_TEAM = 2BQHEV6DNR;
603 | GENERATE_INFOPLIST_FILE = YES;
604 | LD_RUNPATH_SEARCH_PATHS = (
605 | "$(inherited)",
606 | "@executable_path/../Frameworks",
607 | "@loader_path/../Frameworks",
608 | );
609 | MACOSX_DEPLOYMENT_TARGET = 12.0;
610 | MARKETING_VERSION = 1.0;
611 | PRODUCT_BUNDLE_IDENTIFIER = "danilocampos.Macropad-ToolboxTests";
612 | PRODUCT_NAME = "$(TARGET_NAME)";
613 | SWIFT_EMIT_LOC_STRINGS = NO;
614 | SWIFT_VERSION = 5.0;
615 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Macropad Toolbox.app/Contents/MacOS/Macropad Toolbox";
616 | };
617 | name = Release;
618 | };
619 | 1FFBF876276E6E3800918BE8 /* Debug */ = {
620 | isa = XCBuildConfiguration;
621 | buildSettings = {
622 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
623 | CODE_SIGN_STYLE = Automatic;
624 | COMBINE_HIDPI_IMAGES = YES;
625 | CURRENT_PROJECT_VERSION = 1;
626 | DEVELOPMENT_TEAM = 2BQHEV6DNR;
627 | GENERATE_INFOPLIST_FILE = YES;
628 | LD_RUNPATH_SEARCH_PATHS = (
629 | "$(inherited)",
630 | "@executable_path/../Frameworks",
631 | "@loader_path/../Frameworks",
632 | );
633 | MARKETING_VERSION = 1.0;
634 | PRODUCT_BUNDLE_IDENTIFIER = "danilocampos.Macropad-ToolboxUITests";
635 | PRODUCT_NAME = "$(TARGET_NAME)";
636 | SWIFT_EMIT_LOC_STRINGS = NO;
637 | SWIFT_VERSION = 5.0;
638 | TEST_TARGET_NAME = "Macropad Toolbox";
639 | };
640 | name = Debug;
641 | };
642 | 1FFBF877276E6E3800918BE8 /* Release */ = {
643 | isa = XCBuildConfiguration;
644 | buildSettings = {
645 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
646 | CODE_SIGN_STYLE = Automatic;
647 | COMBINE_HIDPI_IMAGES = YES;
648 | CURRENT_PROJECT_VERSION = 1;
649 | DEVELOPMENT_TEAM = 2BQHEV6DNR;
650 | GENERATE_INFOPLIST_FILE = YES;
651 | LD_RUNPATH_SEARCH_PATHS = (
652 | "$(inherited)",
653 | "@executable_path/../Frameworks",
654 | "@loader_path/../Frameworks",
655 | );
656 | MARKETING_VERSION = 1.0;
657 | PRODUCT_BUNDLE_IDENTIFIER = "danilocampos.Macropad-ToolboxUITests";
658 | PRODUCT_NAME = "$(TARGET_NAME)";
659 | SWIFT_EMIT_LOC_STRINGS = NO;
660 | SWIFT_VERSION = 5.0;
661 | TEST_TARGET_NAME = "Macropad Toolbox";
662 | };
663 | name = Release;
664 | };
665 | /* End XCBuildConfiguration section */
666 |
667 | /* Begin XCConfigurationList section */
668 | 1FFBF840276E6E3700918BE8 /* Build configuration list for PBXProject "Macropad Toolbox" */ = {
669 | isa = XCConfigurationList;
670 | buildConfigurations = (
671 | 1FFBF86D276E6E3800918BE8 /* Debug */,
672 | 1FFBF86E276E6E3800918BE8 /* Release */,
673 | );
674 | defaultConfigurationIsVisible = 0;
675 | defaultConfigurationName = Release;
676 | };
677 | 1FFBF86F276E6E3800918BE8 /* Build configuration list for PBXNativeTarget "Macropad Toolbox" */ = {
678 | isa = XCConfigurationList;
679 | buildConfigurations = (
680 | 1FFBF870276E6E3800918BE8 /* Debug */,
681 | 1FFBF871276E6E3800918BE8 /* Release */,
682 | );
683 | defaultConfigurationIsVisible = 0;
684 | defaultConfigurationName = Release;
685 | };
686 | 1FFBF872276E6E3800918BE8 /* Build configuration list for PBXNativeTarget "Macropad ToolboxTests" */ = {
687 | isa = XCConfigurationList;
688 | buildConfigurations = (
689 | 1FFBF873276E6E3800918BE8 /* Debug */,
690 | 1FFBF874276E6E3800918BE8 /* Release */,
691 | );
692 | defaultConfigurationIsVisible = 0;
693 | defaultConfigurationName = Release;
694 | };
695 | 1FFBF875276E6E3800918BE8 /* Build configuration list for PBXNativeTarget "Macropad ToolboxUITests" */ = {
696 | isa = XCConfigurationList;
697 | buildConfigurations = (
698 | 1FFBF876276E6E3800918BE8 /* Debug */,
699 | 1FFBF877276E6E3800918BE8 /* Release */,
700 | );
701 | defaultConfigurationIsVisible = 0;
702 | defaultConfigurationName = Release;
703 | };
704 | /* End XCConfigurationList section */
705 |
706 | /* Begin XCVersionGroup section */
707 | 1FFBF853276E6E3800918BE8 /* Macropad_Toolbox.xcdatamodeld */ = {
708 | isa = XCVersionGroup;
709 | children = (
710 | 1FFBF854276E6E3800918BE8 /* Macropad_Toolbox.xcdatamodel */,
711 | );
712 | currentVersion = 1FFBF854276E6E3800918BE8 /* Macropad_Toolbox.xcdatamodel */;
713 | path = Macropad_Toolbox.xcdatamodeld;
714 | sourceTree = "";
715 | versionGroupType = wrapper.xcdatamodel;
716 | };
717 | /* End XCVersionGroup section */
718 | };
719 | rootObject = 1FFBF83D276E6E3700918BE8 /* Project object */;
720 | }
721 |
--------------------------------------------------------------------------------