From 9bf3fba055f20ef3a53c602e3500300224f5a461 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Sat, 4 Jul 2026 17:00:42 -0400 Subject: [PATCH 01/11] feat(mac): Created new ConfigView --- mac/Config/Config.xcodeproj/project.pbxproj | 30 +++++-- mac/Config/Config/ConfigApp.swift | 4 + .../ConfigTests/ConfigTests.swift | 0 mac/Config/Config/MainConfigView.swift | 83 +++++++++++++++++++ mac/Config/Config/PackageButtonView.swift | 34 ++++++++ 5 files changed, 142 insertions(+), 9 deletions(-) rename mac/Config/{ => Config}/ConfigTests/ConfigTests.swift (100%) create mode 100644 mac/Config/Config/MainConfigView.swift create mode 100644 mac/Config/Config/PackageButtonView.swift diff --git a/mac/Config/Config.xcodeproj/project.pbxproj b/mac/Config/Config.xcodeproj/project.pbxproj index 56b4e676045..691e04b9ebe 100644 --- a/mac/Config/Config.xcodeproj/project.pbxproj +++ b/mac/Config/Config.xcodeproj/project.pbxproj @@ -33,6 +33,23 @@ D88F03DD2F50ED5100C02A31 /* ConfigUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfigUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 375D21122FF2F21800FCD24A /* Exceptions for "Config" folder in "Config" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + ConfigTests/ConfigTests.swift, + ); + target = D88F03C52F50ED5000C02A31 /* Config */; + }; + 375D21132FF2F21800FCD24A /* Exceptions for "Config" folder in "ConfigTests" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + ConfigTests/ConfigTests.swift, + ); + target = D88F03D22F50ED5100C02A31 /* ConfigTests */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ D87D6F492FAF95400083A95E /* Installation */ = { isa = PBXFileSystemSynchronizedRootGroup; @@ -41,14 +58,13 @@ }; D88F03C82F50ED5000C02A31 /* Config */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 375D21122FF2F21800FCD24A /* Exceptions for "Config" folder in "Config" target */, + 375D21132FF2F21800FCD24A /* Exceptions for "Config" folder in "ConfigTests" target */, + ); path = Config; sourceTree = ""; }; - D88F03D62F50ED5100C02A31 /* ConfigTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = ConfigTests; - sourceTree = ""; - }; D88F03E02F50ED5100C02A31 /* ConfigUITests */ = { isa = PBXFileSystemSynchronizedRootGroup; path = ConfigUITests; @@ -87,7 +103,6 @@ children = ( D87D6F492FAF95400083A95E /* Installation */, D88F03C82F50ED5000C02A31 /* Config */, - D88F03D62F50ED5100C02A31 /* ConfigTests */, D88F03E02F50ED5100C02A31 /* ConfigUITests */, D88F04022F512FE800C02A31 /* Frameworks */, D88F03C72F50ED5000C02A31 /* Products */, @@ -151,9 +166,6 @@ dependencies = ( D88F03D52F50ED5100C02A31 /* PBXTargetDependency */, ); - fileSystemSynchronizedGroups = ( - D88F03D62F50ED5100C02A31 /* ConfigTests */, - ); name = ConfigTests; packageProductDependencies = ( ); diff --git a/mac/Config/Config/ConfigApp.swift b/mac/Config/Config/ConfigApp.swift index eb771fac575..c3a46e9d79d 100644 --- a/mac/Config/Config/ConfigApp.swift +++ b/mac/Config/Config/ConfigApp.swift @@ -25,6 +25,10 @@ struct ConfigApp: App { } } } + Window("Main Configuration", id: "main_config") { + MainConfigView() + .environmentObject(settings) + } Window("Installation", id: "install") { InstallView() .environmentObject(installation) diff --git a/mac/Config/ConfigTests/ConfigTests.swift b/mac/Config/Config/ConfigTests/ConfigTests.swift similarity index 100% rename from mac/Config/ConfigTests/ConfigTests.swift rename to mac/Config/Config/ConfigTests/ConfigTests.swift diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift new file mode 100644 index 00000000000..373d90af5c6 --- /dev/null +++ b/mac/Config/Config/MainConfigView.swift @@ -0,0 +1,83 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Gabriel Schantz on 2026-06-29 + * + * Main view used for configuring Keyman + * TODO: Finish writing file summary + */ + +import SwiftUI +import KeymanSettings + +struct MainConfigView: View { + @EnvironmentObject var settings: SettingsContainer + @State private var isShowingSheet = false + @State private var packageToDeleteIndex: Int? + + var body: some View { + + Button { + isShowingSheet = true + } label: { + Label("Add Keyboard", systemImage: "plus") + } + .buttonStyle(.bordered) + .clipShape(Capsule()) + .padding() + // Binds the visibility state to the sheet builder + .sheet(isPresented: $isShowingSheet) { + InstallKeyboardView() + .frame(width: 960, height: 390) + // FEAT/MAC/CONFIG-WINDOW TODO: Make width and height percentages + } + + List(Array(zip(settings.installedPackages.indices, settings.installedPackages)), id: \.1.id) { index, package in + ForEach(package.keyboards) { keyboard in + DisclosureGroup { + Text("Keyboard Info Goes Here") + } label: { + HStack { + + Text(keyboard.name) + .font(.title2) + + Spacer() + + // view keyboard info button + PackageButtonView(action: { print("View info") }, label: "View info", systemImage: "info.circle", helpText: "View info") + + // delete keyboard button + PackageButtonView(action: { packageToDeleteIndex = index }, label: "Delete keyboard", systemImage: "trash", helpText: "Delete keyboard") + // FEAT/MAC/CONFIG-WINDOW TODO: Extract and clean up alert modifier + .alert("Are you sure you want to delete the keyboard '\(package.packageName)'?", + isPresented: Binding( + get: { packageToDeleteIndex == index }, + set: { if !$0 { packageToDeleteIndex = nil } } + )) { + // cancel button + Button("Cancel", role: .cancel) { + packageToDeleteIndex = nil + } + // delete button + Button("Delete", role: .destructive) { + settings.removePackage(at: index) + packageToDeleteIndex = nil + } + } message: { + Text("You can't undo this action.") + } + + } + } + } + } + .padding() + } +} + +#Preview { + var settings = SettingsContainer() + MainConfigView() + .environmentObject(settings) +} diff --git a/mac/Config/Config/PackageButtonView.swift b/mac/Config/Config/PackageButtonView.swift new file mode 100644 index 00000000000..d67f21fb45d --- /dev/null +++ b/mac/Config/Config/PackageButtonView.swift @@ -0,0 +1,34 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Gabriel Schantz on 2026-07-03 + * + * Extracted views used to compose smaller views in MainConfigView + * TODO: Finish writing file summary + */ + +import SwiftUI + +public struct PackageButtonView: View { + let action: () -> Void + let label: String + let systemImage: String + let helpText: String + + public var body: some View { + + Button { + action() + } label: { + Label(label, systemImage: systemImage) + .labelStyle(.iconOnly) + .font(.title2) + } + .buttonStyle(.bordered) + .clipShape(.circle) + .contentShape(Capsule()) + // feat/mac/config-window TODO: Decide proper help text for usages of PackageButtonView... + .help(helpText) + + } +} From 86b16dffb8c6e1604f190c96217b6d32f1210a7e Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Mon, 20 Jul 2026 13:15:15 -0400 Subject: [PATCH 02/11] feat(mac): refactored alert modifier bound to the delete keyboard button and changed it to work with UUID instead of index --- mac/Config/Config/MainConfigView.swift | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 373d90af5c6..364fdbce9da 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -13,10 +13,15 @@ import KeymanSettings struct MainConfigView: View { @EnvironmentObject var settings: SettingsContainer @State private var isShowingSheet = false - @State private var packageToDeleteIndex: Int? + @State private var selectedPackage: KeymanPackage? + @State private var isShowingAlert = false + + private func showAlert(for package: KeymanPackage) -> Void { + isShowingAlert = true + selectedPackage = package + } var body: some View { - Button { isShowingSheet = true } label: { @@ -32,7 +37,7 @@ struct MainConfigView: View { // FEAT/MAC/CONFIG-WINDOW TODO: Make width and height percentages } - List(Array(zip(settings.installedPackages.indices, settings.installedPackages)), id: \.1.id) { index, package in + List(Array(zip(settings.singleKeyboardPackages.indices, settings.singleKeyboardPackages)), id: \.1.id) { index, package in ForEach(package.keyboards) { keyboard in DisclosureGroup { Text("Keyboard Info Goes Here") @@ -41,37 +46,32 @@ struct MainConfigView: View { Text(keyboard.name) .font(.title2) - + Spacer() - + // view keyboard info button PackageButtonView(action: { print("View info") }, label: "View info", systemImage: "info.circle", helpText: "View info") - + // delete keyboard button - PackageButtonView(action: { packageToDeleteIndex = index }, label: "Delete keyboard", systemImage: "trash", helpText: "Delete keyboard") - // FEAT/MAC/CONFIG-WINDOW TODO: Extract and clean up alert modifier - .alert("Are you sure you want to delete the keyboard '\(package.packageName)'?", - isPresented: Binding( - get: { packageToDeleteIndex == index }, - set: { if !$0 { packageToDeleteIndex = nil } } - )) { - // cancel button - Button("Cancel", role: .cancel) { - packageToDeleteIndex = nil - } - // delete button - Button("Delete", role: .destructive) { - settings.removePackage(at: index) - packageToDeleteIndex = nil - } - } message: { - Text("You can't undo this action.") - } - + PackageButtonView(action: { showAlert(for: package) }, label: "Delete keyboard", systemImage: "trash", helpText: "Delete keyboard") } } } } + // FEAT?/MAC/CONFIG-WINDOW TODO: Make the alert title display the keyboard name + .alert("Are you sure you want to delete the keyboard \"\(selectedPackage?.packageName ?? "")\"?", + isPresented: $isShowingAlert, + presenting: selectedPackage) { package in + + Button("Cancel", role: .cancel) { } + + Button("Delete", role: .destructive) { + // FEAT/MAC/CONFIG-WINDOW TODO: Make removeInstalledPackage based on id + //settings.removeInstalledPackage(at: index) + } + } message: { package in + Text("You can't undo this action.") + } .padding() } } From 66e665a81f700b862c043c4355cb87edfa4b4dc2 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Tue, 21 Jul 2026 15:00:59 -0400 Subject: [PATCH 03/11] feat(mac): created the keyboard info view and did minor refactoring --- ...eButtonView.swift => IconButtonView.swift} | 8 +-- mac/Config/Config/KeyboardInfoView.swift | 54 +++++++++++++++++++ mac/Config/Config/MainConfigView.swift | 12 ++--- 3 files changed, 64 insertions(+), 10 deletions(-) rename mac/Config/Config/{PackageButtonView.swift => IconButtonView.swift} (72%) create mode 100644 mac/Config/Config/KeyboardInfoView.swift diff --git a/mac/Config/Config/PackageButtonView.swift b/mac/Config/Config/IconButtonView.swift similarity index 72% rename from mac/Config/Config/PackageButtonView.swift rename to mac/Config/Config/IconButtonView.swift index d67f21fb45d..617b51e324a 100644 --- a/mac/Config/Config/PackageButtonView.swift +++ b/mac/Config/Config/IconButtonView.swift @@ -3,13 +3,13 @@ * * Created by Gabriel Schantz on 2026-07-03 * - * Extracted views used to compose smaller views in MainConfigView - * TODO: Finish writing file summary + * View for icon buttons + * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary */ import SwiftUI -public struct PackageButtonView: View { +public struct IconButtonView: View { let action: () -> Void let label: String let systemImage: String @@ -27,7 +27,7 @@ public struct PackageButtonView: View { .buttonStyle(.bordered) .clipShape(.circle) .contentShape(Capsule()) - // feat/mac/config-window TODO: Decide proper help text for usages of PackageButtonView... + // FEAT/MAC/CONFIG-WINDOW TODO: Decide proper help text for usages of PackageButtonView... .help(helpText) } diff --git a/mac/Config/Config/KeyboardInfoView.swift b/mac/Config/Config/KeyboardInfoView.swift new file mode 100644 index 00000000000..55056a7a0f0 --- /dev/null +++ b/mac/Config/Config/KeyboardInfoView.swift @@ -0,0 +1,54 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Gabriel Schantz on 2026-07-20 + * + * + * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary + */ + +import SwiftUI +import KeymanSettings + +public struct KeyboardInfoView: View { + let package: KeymanPackage + + public init(for package: KeymanPackage) { + self.package = package + } + + public var body: some View { + HStack { + // the custom package image + if let packageImage = package.graphicImage { + Image(nsImage: packageImage) + .resizable() + .frame(width: 140, height: 250) + .border(Color.red, width: 1) + } + // the package properties presented as text + VStack (alignment: .leading, spacing: 10) { + Text("Keyboard Version: \(package.packageVersion)") + Text("Fonts:") + ForEach(package.fonts, id: \.self) { font in + Text(font) + } + Text("Author: \(package.author ?? "")") + Text("Copyright: \(package.copyright ?? "")") + // FEAT/MAC/CONFIG-WINDOW TODO: Change text to link with Author's Website + Text("Author's Website") + } + .border(Color.blue, width: 1) + // the package QR Code + // FEAT?MAC?CONFIG-WINDOW TODO: Make size variable + if let qrCode = package.generateSharePackageQRCode(size: 200) { + Image(nsImage: qrCode) + .interpolation(.none) // important: ensures the edges of the QR Code remain sharp + .resizable() + .frame(width: 200, height: 200) + .background(Color.white) // ensures good contrast for scanning + .border(Color.green, width: 1) + } + } + } +} diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 364fdbce9da..59a6f188814 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -4,7 +4,7 @@ * Created by Gabriel Schantz on 2026-06-29 * * Main view used for configuring Keyman - * TODO: Finish writing file summary + * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary */ import SwiftUI @@ -13,8 +13,8 @@ import KeymanSettings struct MainConfigView: View { @EnvironmentObject var settings: SettingsContainer @State private var isShowingSheet = false - @State private var selectedPackage: KeymanPackage? @State private var isShowingAlert = false + @State private var selectedPackage: KeymanPackage? private func showAlert(for package: KeymanPackage) -> Void { isShowingAlert = true @@ -40,7 +40,7 @@ struct MainConfigView: View { List(Array(zip(settings.singleKeyboardPackages.indices, settings.singleKeyboardPackages)), id: \.1.id) { index, package in ForEach(package.keyboards) { keyboard in DisclosureGroup { - Text("Keyboard Info Goes Here") + KeyboardInfoView(for: package) } label: { HStack { @@ -49,11 +49,11 @@ struct MainConfigView: View { Spacer() - // view keyboard info button - PackageButtonView(action: { print("View info") }, label: "View info", systemImage: "info.circle", helpText: "View info") + // see keyboard help button + IconButtonView(action: { print("See help") }, label: "See help", systemImage: "questionmark.circle", helpText: "See help") // delete keyboard button - PackageButtonView(action: { showAlert(for: package) }, label: "Delete keyboard", systemImage: "trash", helpText: "Delete keyboard") + IconButtonView(action: { showAlert(for: package) }, label: "Delete keyboard", systemImage: "trash", helpText: "Delete keyboard") } } } From dcc1534c36a1fd2f68650db89d8459302a6c692e Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Tue, 21 Jul 2026 15:22:47 -0400 Subject: [PATCH 04/11] feat(mac): added remove installed package support --- mac/Config/Config/MainConfigView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 59a6f188814..ef45bd40645 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -66,8 +66,7 @@ struct MainConfigView: View { Button("Cancel", role: .cancel) { } Button("Delete", role: .destructive) { - // FEAT/MAC/CONFIG-WINDOW TODO: Make removeInstalledPackage based on id - //settings.removeInstalledPackage(at: index) + settings.removeInstalledPackage(with: package.id) } } message: { package in Text("You can't undo this action.") From e39ae0395bd82031496d11205a4d20c1bdc509f4 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Tue, 21 Jul 2026 16:02:22 -0400 Subject: [PATCH 05/11] feat(mac): removed unnecessary complexity no longer required because of changes to the way installed packages are removed --- mac/Config/Config/MainConfigView.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index ef45bd40645..34e504a46f1 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -15,7 +15,10 @@ struct MainConfigView: View { @State private var isShowingSheet = false @State private var isShowingAlert = false @State private var selectedPackage: KeymanPackage? - + + /** + * Sets isShowingAlert to true and assigns the state variable selectedPackage the KeymanPackage passed in as a parameter + */ private func showAlert(for package: KeymanPackage) -> Void { isShowingAlert = true selectedPackage = package @@ -37,7 +40,7 @@ struct MainConfigView: View { // FEAT/MAC/CONFIG-WINDOW TODO: Make width and height percentages } - List(Array(zip(settings.singleKeyboardPackages.indices, settings.singleKeyboardPackages)), id: \.1.id) { index, package in + List(settings.singleKeyboardPackages, id: \.id) { package in ForEach(package.keyboards) { keyboard in DisclosureGroup { KeyboardInfoView(for: package) @@ -58,7 +61,6 @@ struct MainConfigView: View { } } } - // FEAT?/MAC/CONFIG-WINDOW TODO: Make the alert title display the keyboard name .alert("Are you sure you want to delete the keyboard \"\(selectedPackage?.packageName ?? "")\"?", isPresented: $isShowingAlert, presenting: selectedPackage) { package in From 81a9d48b511daf3ac6457c099a15e8d8bf80fb98 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Thu, 23 Jul 2026 12:32:24 -0400 Subject: [PATCH 06/11] feat(mac): layout changes to the package info view --- mac/Config/Config/IconButtonView.swift | 3 +- mac/Config/Config/KeyboardInfoView.swift | 100 ++++++++++++++++++----- mac/Config/Config/MainConfigView.swift | 18 ++-- 3 files changed, 92 insertions(+), 29 deletions(-) diff --git a/mac/Config/Config/IconButtonView.swift b/mac/Config/Config/IconButtonView.swift index 617b51e324a..aa268b3947a 100644 --- a/mac/Config/Config/IconButtonView.swift +++ b/mac/Config/Config/IconButtonView.swift @@ -14,6 +14,7 @@ public struct IconButtonView: View { let label: String let systemImage: String let helpText: String + let font: Font public var body: some View { @@ -22,7 +23,7 @@ public struct IconButtonView: View { } label: { Label(label, systemImage: systemImage) .labelStyle(.iconOnly) - .font(.title2) + .font(font) } .buttonStyle(.bordered) .clipShape(.circle) diff --git a/mac/Config/Config/KeyboardInfoView.swift b/mac/Config/Config/KeyboardInfoView.swift index 55056a7a0f0..601465648d5 100644 --- a/mac/Config/Config/KeyboardInfoView.swift +++ b/mac/Config/Config/KeyboardInfoView.swift @@ -17,38 +17,98 @@ public struct KeyboardInfoView: View { self.package = package } + /** + * Copies the text argument to the system clipboard + */ + private func copyTextToClipboard (text: String) -> Void { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(text, forType: .string) + } + public var body: some View { + HStack { + // the custom package image if let packageImage = package.graphicImage { Image(nsImage: packageImage) .resizable() - .frame(width: 140, height: 250) - .border(Color.red, width: 1) + .frame(width: 87.92, height: 157) // Corresponds to max height of package properties presented as text and maintains width:height ratio of 140:250 } + // the package properties presented as text - VStack (alignment: .leading, spacing: 10) { - Text("Keyboard Version: \(package.packageVersion)") - Text("Fonts:") - ForEach(package.fonts, id: \.self) { font in - Text(font) + Grid(horizontalSpacing: 10, verticalSpacing: 5) { + // the package version + GridRow { + Text("Package Version:").bold() + .gridColumnAlignment(.trailing) + Text(package.packageVersion) + .gridColumnAlignment(.leading) + } + // the fonts + GridRow { + Text("Fonts:").bold() + HStack{ + ForEach(package.fonts, id: \.self) { font in + Text(font) + } + } + } + // the copyright + GridRow { + Text("Copyright:").bold() + Text(package.copyright ?? "") + } + // the author + GridRow { + Text("Author:").bold() + Text(package.author ?? "") + } + // the website + GridRow { + Text("Website:").bold() + if let websiteUrl = package.websiteUrl { + Link(destination: websiteUrl) { + Text(websiteUrl.absoluteString) + .underline() + .multilineTextAlignment(.leading) + } + } } - Text("Author: \(package.author ?? "")") - Text("Copyright: \(package.copyright ?? "")") - // FEAT/MAC/CONFIG-WINDOW TODO: Change text to link with Author's Website - Text("Author's Website") } - .border(Color.blue, width: 1) - // the package QR Code + .frame(minWidth: 350, minHeight: 125) + .padding() + + Spacer() + + // the package QR Code and link to share the package online // FEAT?MAC?CONFIG-WINDOW TODO: Make size variable - if let qrCode = package.generateSharePackageQRCode(size: 200) { - Image(nsImage: qrCode) - .interpolation(.none) // important: ensures the edges of the QR Code remain sharp - .resizable() - .frame(width: 200, height: 200) - .background(Color.white) // ensures good contrast for scanning - .border(Color.green, width: 1) + VStack { + + if let qrCode = package.generateSharePackageQRCode(size: 113) { + Image(nsImage: qrCode) + .interpolation(.none) // important: ensures the edges of the QR Code remain sharp + .resizable() + .frame(width: 113, height: 113) + .background(Color.white) // ensures good contrast for scanning + } + + if let sharePackageUrl = package.sharePackageUrl { + + HStack { + + Link(destination: sharePackageUrl) { + Text("Share Keyboard") + .underline() + } + IconButtonView(action: { copyTextToClipboard(text: sharePackageUrl.absoluteString) }, label: "Copy link", systemImage: "doc.on.doc", helpText: "Copy link", font: .body) + } + } } + .padding(5) + .border(Color.black, width: 1) + //.padding([.top, .bottom]) } } } diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 34e504a46f1..3d58a9f7108 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -5,6 +5,7 @@ * * Main view used for configuring Keyman * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary + * FEAT/MAC/CONFIG-WINDOW TODO: Set minimim width and height for window */ import SwiftUI @@ -13,14 +14,14 @@ import KeymanSettings struct MainConfigView: View { @EnvironmentObject var settings: SettingsContainer @State private var isShowingSheet = false - @State private var isShowingAlert = false + @State private var isShowingDeleteAlert = false @State private var selectedPackage: KeymanPackage? /** - * Sets isShowingAlert to true and assigns the state variable selectedPackage the KeymanPackage passed in as a parameter + * Sets isShowingDeleteAlert to true and assigns the state variable selectedPackage the KeymanPackage argument */ - private func showAlert(for package: KeymanPackage) -> Void { - isShowingAlert = true + private func showDeleteAlert(for package: KeymanPackage) -> Void { + isShowingDeleteAlert = true selectedPackage = package } @@ -29,6 +30,7 @@ struct MainConfigView: View { isShowingSheet = true } label: { Label("Add Keyboard", systemImage: "plus") + .font(.title2) } .buttonStyle(.bordered) .clipShape(Capsule()) @@ -48,21 +50,21 @@ struct MainConfigView: View { HStack { Text(keyboard.name) - .font(.title2) + .font(.title) Spacer() // see keyboard help button - IconButtonView(action: { print("See help") }, label: "See help", systemImage: "questionmark.circle", helpText: "See help") + IconButtonView(action: { print("See help") }, label: "See help", systemImage: "questionmark.circle", helpText: "See help", font: .title2) // delete keyboard button - IconButtonView(action: { showAlert(for: package) }, label: "Delete keyboard", systemImage: "trash", helpText: "Delete keyboard") + IconButtonView(action: { showDeleteAlert(for: package) }, label: "Delete keyboard", systemImage: "trash", helpText: "Delete keyboard", font: .title2) } } } } .alert("Are you sure you want to delete the keyboard \"\(selectedPackage?.packageName ?? "")\"?", - isPresented: $isShowingAlert, + isPresented: $isShowingDeleteAlert, presenting: selectedPackage) { package in Button("Cancel", role: .cancel) { } From 77cb9c35e1ddddab5c71ecb6dabd1d5f263310a6 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Thu, 23 Jul 2026 16:44:00 -0400 Subject: [PATCH 07/11] feat(mac): clicking row toggles disclosure group --- mac/Config/Config/KeyboardInfoView.swift | 4 ++-- mac/Config/Config/MainConfigView.swift | 29 ++++++++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/mac/Config/Config/KeyboardInfoView.swift b/mac/Config/Config/KeyboardInfoView.swift index 601465648d5..1ca821f4e41 100644 --- a/mac/Config/Config/KeyboardInfoView.swift +++ b/mac/Config/Config/KeyboardInfoView.swift @@ -42,9 +42,9 @@ public struct KeyboardInfoView: View { // the package version GridRow { Text("Package Version:").bold() - .gridColumnAlignment(.trailing) + .gridColumnAlignment(.trailing) // all elements underneath inherit the .trailing alignment Text(package.packageVersion) - .gridColumnAlignment(.leading) + .gridColumnAlignment(.leading) // all elements underneath inherit the .leading alignment } // the fonts GridRow { diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 3d58a9f7108..627df2297cd 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -15,7 +15,8 @@ struct MainConfigView: View { @EnvironmentObject var settings: SettingsContainer @State private var isShowingSheet = false @State private var isShowingDeleteAlert = false - @State private var selectedPackage: KeymanPackage? + @State private var selectedPackage: KeymanPackage? = nil + @State private var expandedPackageID: UUID? = nil /** * Sets isShowingDeleteAlert to true and assigns the state variable selectedPackage the KeymanPackage argument @@ -44,22 +45,40 @@ struct MainConfigView: View { List(settings.singleKeyboardPackages, id: \.id) { package in ForEach(package.keyboards) { keyboard in - DisclosureGroup { + DisclosureGroup(isExpanded: Binding( + get: { self.expandedPackageID == package.id }, + // handles when the chevron arrow is clicked by the user + // doesn't require if statement because the disclosure group has a state to toggle which is passed into the setter + // $0 = true when disclosure group is open and $0 = false when disclosure group is closed + set: { self.expandedPackageID = $0 ? package.id : nil } + )) { KeyboardInfoView(for: package) } label: { HStack { Text(keyboard.name) .font(.title) - + Spacer() - + // see keyboard help button IconButtonView(action: { print("See help") }, label: "See help", systemImage: "questionmark.circle", helpText: "See help", font: .title2) - + // delete keyboard button IconButtonView(action: { showDeleteAlert(for: package) }, label: "Delete keyboard", systemImage: "trash", helpText: "Delete keyboard", font: .title2) } + .contentShape(Rectangle()) + // handles when the HStack is clicked by the user + .onTapGesture { + withAnimation { + // requires if statement because the HStack doesn't have a state to toggle and so the package id property must be manually checked + if self.expandedPackageID == package.id { + self.expandedPackageID = nil + } else { + self.expandedPackageID = package.id + } + } + } } } } From 6ff671428c91d86caa6a789656496b9f7bd3332e Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Fri, 24 Jul 2026 11:27:58 -0400 Subject: [PATCH 08/11] feat(mac): refactored the icon button view --- mac/Config/Config/IconButtonView.swift | 19 +++++++++------ mac/Config/Config/KeyboardInfoView.swift | 8 +++--- mac/Config/Config/MainConfigView.swift | 31 ++++++++++++------------ 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/mac/Config/Config/IconButtonView.swift b/mac/Config/Config/IconButtonView.swift index aa268b3947a..0e62a65d93e 100644 --- a/mac/Config/Config/IconButtonView.swift +++ b/mac/Config/Config/IconButtonView.swift @@ -3,32 +3,35 @@ * * Created by Gabriel Schantz on 2026-07-03 * - * View for icon buttons - * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary + * The view used for image-only buttons + * The view relies on the automatically sythesized memberwise initializer + * + * Parameters: + * - action: The closure to perform when the button is clicked + * - systemImage: The String used for the SF Symbol to display + * - font: The Font used for the image + * - helpText: The String used for assistive technologies (e.g. VoiceOver) and shown as a tooltip */ import SwiftUI public struct IconButtonView: View { let action: () -> Void - let label: String let systemImage: String - let helpText: String let font: Font + let helpText: String public var body: some View { Button { action() } label: { - Label(label, systemImage: systemImage) - .labelStyle(.iconOnly) + Image(systemName: systemImage) .font(font) } .buttonStyle(.bordered) .clipShape(.circle) - .contentShape(Capsule()) - // FEAT/MAC/CONFIG-WINDOW TODO: Decide proper help text for usages of PackageButtonView... + .accessibilityLabel(helpText) .help(helpText) } diff --git a/mac/Config/Config/KeyboardInfoView.swift b/mac/Config/Config/KeyboardInfoView.swift index 1ca821f4e41..f8bb564673f 100644 --- a/mac/Config/Config/KeyboardInfoView.swift +++ b/mac/Config/Config/KeyboardInfoView.swift @@ -25,7 +25,7 @@ public struct KeyboardInfoView: View { pasteboard.clearContents() pasteboard.setString(text, forType: .string) } - + public var body: some View { HStack { @@ -77,8 +77,8 @@ public struct KeyboardInfoView: View { } } } - .frame(minWidth: 350, minHeight: 125) - .padding() + .frame(minWidth: 350, minHeight: 125) + .padding() Spacer() @@ -102,7 +102,7 @@ public struct KeyboardInfoView: View { Text("Share Keyboard") .underline() } - IconButtonView(action: { copyTextToClipboard(text: sharePackageUrl.absoluteString) }, label: "Copy link", systemImage: "doc.on.doc", helpText: "Copy link", font: .body) + IconButtonView(action: { copyTextToClipboard(text: sharePackageUrl.absoluteString) }, systemImage: "doc.on.doc", font: .body , helpText: "Copy link") } } } diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 627df2297cd..f335e0a1272 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -17,7 +17,7 @@ struct MainConfigView: View { @State private var isShowingDeleteAlert = false @State private var selectedPackage: KeymanPackage? = nil @State private var expandedPackageID: UUID? = nil - + /** * Sets isShowingDeleteAlert to true and assigns the state variable selectedPackage the KeymanPackage argument */ @@ -36,7 +36,7 @@ struct MainConfigView: View { .buttonStyle(.bordered) .clipShape(Capsule()) .padding() - // Binds the visibility state to the sheet builder + // binds the visibility state to the sheet builder .sheet(isPresented: $isShowingSheet) { InstallKeyboardView() .frame(width: 960, height: 390) @@ -47,7 +47,7 @@ struct MainConfigView: View { ForEach(package.keyboards) { keyboard in DisclosureGroup(isExpanded: Binding( get: { self.expandedPackageID == package.id }, - // handles when the chevron arrow is clicked by the user + // setter handles when the chevron arrow is clicked by the user // doesn't require if statement because the disclosure group has a state to toggle which is passed into the setter // $0 = true when disclosure group is open and $0 = false when disclosure group is closed set: { self.expandedPackageID = $0 ? package.id : nil } @@ -62,10 +62,10 @@ struct MainConfigView: View { Spacer() // see keyboard help button - IconButtonView(action: { print("See help") }, label: "See help", systemImage: "questionmark.circle", helpText: "See help", font: .title2) + IconButtonView(action: { print("See help") }, systemImage: "questionmark.circle", font: .title2, helpText: "See help") // delete keyboard button - IconButtonView(action: { showDeleteAlert(for: package) }, label: "Delete keyboard", systemImage: "trash", helpText: "Delete keyboard", font: .title2) + IconButtonView(action: { showDeleteAlert(for: package) }, systemImage: "trash", font: .title2, helpText: "Delete keyboard") } .contentShape(Rectangle()) // handles when the HStack is clicked by the user @@ -82,18 +82,19 @@ struct MainConfigView: View { } } } + // binds the visibilty state to the alert builder .alert("Are you sure you want to delete the keyboard \"\(selectedPackage?.packageName ?? "")\"?", - isPresented: $isShowingDeleteAlert, - presenting: selectedPackage) { package in - - Button("Cancel", role: .cancel) { } - - Button("Delete", role: .destructive) { - settings.removeInstalledPackage(with: package.id) - } - } message: { package in - Text("You can't undo this action.") + isPresented: $isShowingDeleteAlert, + presenting: selectedPackage) { package in + + Button("Cancel", role: .cancel) { } + + Button("Delete", role: .destructive) { + settings.removeInstalledPackage(with: package.id) } + } message: { package in + Text("You can't undo this action.") + } .padding() } } From f9579b3de9041a3bd3202678c139c5b8771daeee Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Fri, 24 Jul 2026 15:34:10 -0400 Subject: [PATCH 09/11] feat(mac): layout changes to the package info view --- mac/Config/Config/KeyboardInfoView.swift | 35 ++++--- mac/Config/Config/MainConfigView.swift | 128 +++++++++++------------ 2 files changed, 84 insertions(+), 79 deletions(-) diff --git a/mac/Config/Config/KeyboardInfoView.swift b/mac/Config/Config/KeyboardInfoView.swift index f8bb564673f..471f5c208fa 100644 --- a/mac/Config/Config/KeyboardInfoView.swift +++ b/mac/Config/Config/KeyboardInfoView.swift @@ -3,6 +3,8 @@ * * Created by Gabriel Schantz on 2026-07-20 * + * The view used for keyboard info + * The view relies on the automatically synthesized memberwise intializer * * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary */ @@ -13,10 +15,6 @@ import KeymanSettings public struct KeyboardInfoView: View { let package: KeymanPackage - public init(for package: KeymanPackage) { - self.package = package - } - /** * Copies the text argument to the system clipboard */ @@ -28,16 +26,16 @@ public struct KeyboardInfoView: View { public var body: some View { - HStack { + HStack (alignment: .top) { // the custom package image if let packageImage = package.graphicImage { Image(nsImage: packageImage) .resizable() - .frame(width: 87.92, height: 157) // Corresponds to max height of package properties presented as text and maintains width:height ratio of 140:250 + .frame(maxWidth: 84, maxHeight: 150) } - // the package properties presented as text + // the text-based package properties presented in a grid Grid(horizontalSpacing: 10, verticalSpacing: 5) { // the package version GridRow { @@ -46,25 +44,29 @@ public struct KeyboardInfoView: View { Text(package.packageVersion) .gridColumnAlignment(.leading) // all elements underneath inherit the .leading alignment } + // the fonts GridRow { Text("Fonts:").bold() - HStack{ + HStack { ForEach(package.fonts, id: \.self) { font in Text(font) } } } + // the copyright GridRow { Text("Copyright:").bold() Text(package.copyright ?? "") } + // the author GridRow { Text("Author:").bold() Text(package.author ?? "") } + // the website GridRow { Text("Website:").bold() @@ -77,20 +79,20 @@ public struct KeyboardInfoView: View { } } } - .frame(minWidth: 350, minHeight: 125) - .padding() + .padding(5) Spacer() // the package QR Code and link to share the package online - // FEAT?MAC?CONFIG-WINDOW TODO: Make size variable VStack { - - if let qrCode = package.generateSharePackageQRCode(size: 113) { + let size: CGFloat = 106 + if let qrCode = package.generateSharePackageQRCode(size: size) { + + // the package QR Code Image(nsImage: qrCode) .interpolation(.none) // important: ensures the edges of the QR Code remain sharp .resizable() - .frame(width: 113, height: 113) + .frame(width: size, height: size) .background(Color.white) // ensures good contrast for scanning } @@ -98,17 +100,20 @@ public struct KeyboardInfoView: View { HStack { + // the link to share the package online Link(destination: sharePackageUrl) { Text("Share Keyboard") .underline() } + + // the button to copy the link to share the package online IconButtonView(action: { copyTextToClipboard(text: sharePackageUrl.absoluteString) }, systemImage: "doc.on.doc", font: .body , helpText: "Copy link") } } } .padding(5) .border(Color.black, width: 1) - //.padding([.top, .bottom]) } + .frame(height: 150) } } diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index f335e0a1272..f26e8c45ca2 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -5,7 +5,7 @@ * * Main view used for configuring Keyman * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary - * FEAT/MAC/CONFIG-WINDOW TODO: Set minimim width and height for window + * FEAT/MAC/CONFIG-WINDOW TODO: Set width and height for window */ import SwiftUI @@ -21,81 +21,81 @@ struct MainConfigView: View { /** * Sets isShowingDeleteAlert to true and assigns the state variable selectedPackage the KeymanPackage argument */ - private func showDeleteAlert(for package: KeymanPackage) -> Void { + private func showDeleteAlert(for package: KeymanPackage) { isShowingDeleteAlert = true selectedPackage = package } var body: some View { - Button { - isShowingSheet = true - } label: { - Label("Add Keyboard", systemImage: "plus") - .font(.title2) - } - .buttonStyle(.bordered) - .clipShape(Capsule()) - .padding() - // binds the visibility state to the sheet builder - .sheet(isPresented: $isShowingSheet) { - InstallKeyboardView() - .frame(width: 960, height: 390) - // FEAT/MAC/CONFIG-WINDOW TODO: Make width and height percentages - } - - List(settings.singleKeyboardPackages, id: \.id) { package in - ForEach(package.keyboards) { keyboard in - DisclosureGroup(isExpanded: Binding( - get: { self.expandedPackageID == package.id }, - // setter handles when the chevron arrow is clicked by the user - // doesn't require if statement because the disclosure group has a state to toggle which is passed into the setter - // $0 = true when disclosure group is open and $0 = false when disclosure group is closed - set: { self.expandedPackageID = $0 ? package.id : nil } - )) { - KeyboardInfoView(for: package) - } label: { - HStack { - - Text(keyboard.name) - .font(.title) - - Spacer() - - // see keyboard help button - IconButtonView(action: { print("See help") }, systemImage: "questionmark.circle", font: .title2, helpText: "See help") - - // delete keyboard button - IconButtonView(action: { showDeleteAlert(for: package) }, systemImage: "trash", font: .title2, helpText: "Delete keyboard") - } - .contentShape(Rectangle()) - // handles when the HStack is clicked by the user - .onTapGesture { - withAnimation { - // requires if statement because the HStack doesn't have a state to toggle and so the package id property must be manually checked - if self.expandedPackageID == package.id { - self.expandedPackageID = nil - } else { - self.expandedPackageID = package.id + VStack(spacing: 0) { + Button { + isShowingSheet = true + } label: { + Label("Add Keyboard", systemImage: "plus") + .font(.title2) + } + .buttonStyle(.bordered) + .clipShape(Capsule()) + .padding([.top, .leading, .trailing]) + // binds the visibility state to the sheet builder + .sheet(isPresented: $isShowingSheet) { + InstallKeyboardView() + .frame(width: 960, height: 390) + // FEAT/MAC/CONFIG-WINDOW TODO: Make width and height percentages + } + + List(settings.singleKeyboardPackages, id: \.id) { package in + ForEach(package.keyboards) { keyboard in + DisclosureGroup(isExpanded: Binding( + get: { self.expandedPackageID == package.id }, + // setter handles when the chevron arrow is clicked by the user + // $0 = true when disclosure group is open and $0 = false when disclosure group is closed + set: { self.expandedPackageID = $0 ? package.id : nil } + )) { + KeyboardInfoView(package: package) + } label: { + HStack { + + Text(keyboard.name) + .font(.title) + + Spacer() + + // see keyboard help button + IconButtonView(action: { print("Show help") }, systemImage: "questionmark.circle", font: .title2, helpText: "Show help") + + // delete keyboard button + IconButtonView(action: { showDeleteAlert(for: package) }, systemImage: "trash", font: .title2, helpText: "Delete keyboard") + } + .contentShape(Rectangle()) + // handles when the HStack is clicked by the user + .onTapGesture { + withAnimation { + if self.expandedPackageID == package.id { + self.expandedPackageID = nil + } else { + self.expandedPackageID = package.id + } } } } } } - } - // binds the visibilty state to the alert builder - .alert("Are you sure you want to delete the keyboard \"\(selectedPackage?.packageName ?? "")\"?", - isPresented: $isShowingDeleteAlert, - presenting: selectedPackage) { package in - - Button("Cancel", role: .cancel) { } - - Button("Delete", role: .destructive) { - settings.removeInstalledPackage(with: package.id) + // binds the visibilty state to the alert builder + .alert("Are you sure you want to delete the keyboard \"\(selectedPackage?.packageName ?? "")\"?", + isPresented: $isShowingDeleteAlert, + presenting: selectedPackage) { package in + + Button("Cancel", role: .cancel) { } + + Button("Delete", role: .destructive) { + settings.removeInstalledPackage(with: package.id) + } + } message: { package in + Text("You can't undo this action.") } - } message: { package in - Text("You can't undo this action.") + .padding([.leading, .trailing, .bottom]) } - .padding() } } From 687fc1e42cd075e048f5ef0af2efbd42209d4322 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Mon, 27 Jul 2026 10:23:57 -0400 Subject: [PATCH 10/11] feat(mac): extracted label button view and binding for whether or not a package's disclosure group is expanded --- mac/Config/Config/MainConfigView.swift | 62 +++++++++++++++++--------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index f26e8c45ca2..c1b8842f5c1 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -13,9 +13,13 @@ import KeymanSettings struct MainConfigView: View { @EnvironmentObject var settings: SettingsContainer + // visibilty state for the add package sheet @State private var isShowingSheet = false + // visibilty state for the delete package alert @State private var isShowingDeleteAlert = false + // used to identify the selected KeymanPackage for the delete package alert @State private var selectedPackage: KeymanPackage? = nil + // used to identify the expanded KeymanPackage id @State private var expandedPackageID: UUID? = nil /** @@ -28,30 +32,20 @@ struct MainConfigView: View { var body: some View { VStack(spacing: 0) { - Button { - isShowingSheet = true - } label: { - Label("Add Keyboard", systemImage: "plus") - .font(.title2) - } - .buttonStyle(.bordered) - .clipShape(Capsule()) - .padding([.top, .leading, .trailing]) + // the add keyboard button + LabelButtonView(action: { isShowingSheet = true }, label: "Add Keyboard", systemImage: "plus", font: .title2 ) + .clipShape(.capsule) + .padding([.top, .leading, .trailing]) // binds the visibility state to the sheet builder - .sheet(isPresented: $isShowingSheet) { - InstallKeyboardView() - .frame(width: 960, height: 390) - // FEAT/MAC/CONFIG-WINDOW TODO: Make width and height percentages - } + .sheet(isPresented: $isShowingSheet) { + InstallKeyboardView() + .frame(width: 960, height: 390) + // FEAT/MAC/CONFIG-WINDOW TODO: Make width and height percentages + } List(settings.singleKeyboardPackages, id: \.id) { package in ForEach(package.keyboards) { keyboard in - DisclosureGroup(isExpanded: Binding( - get: { self.expandedPackageID == package.id }, - // setter handles when the chevron arrow is clicked by the user - // $0 = true when disclosure group is open and $0 = false when disclosure group is closed - set: { self.expandedPackageID = $0 ? package.id : nil } - )) { + DisclosureGroup(isExpanded: isExpanded(package: package)) { KeyboardInfoView(package: package) } label: { HStack { @@ -97,6 +91,34 @@ struct MainConfigView: View { .padding([.leading, .trailing, .bottom]) } } + + // the view for buttons with a label + public struct LabelButtonView: View { + let action: () -> Void + let label: String + let systemImage: String + let font: Font + + public var body: some View { + Button(action: action) { + Label(label, systemImage: systemImage) + .font(font) + .buttonStyle(.bordered) + } + } + } + + // the helper method to generate the custom binding for whether a package's disclosure group is expanded or not + func isExpanded(package: KeymanPackage) -> Binding { + Binding( + // the getter handles the position of the disclosure group to be rendered + get: { self.expandedPackageID == package.id }, + // setter handles when the chevron arrow is clicked by the user + // $0 = true when disclosure group is open and $0 = false when disclosure group is closed + set: { self.expandedPackageID = $0 ? package.id : nil } + ) + } + } #Preview { From 5d384331aef83b6c1a21064251741925371c6993 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Mon, 27 Jul 2026 16:09:31 -0400 Subject: [PATCH 11/11] feat(mac): toggle button and code reformatting --- mac/Config/Config/IconButtonView.swift | 9 +---- mac/Config/Config/KeyboardInfoView.swift | 4 +- mac/Config/Config/MainConfigView.swift | 50 +++++++++++++++++++++--- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/mac/Config/Config/IconButtonView.swift b/mac/Config/Config/IconButtonView.swift index 0e62a65d93e..6551370c132 100644 --- a/mac/Config/Config/IconButtonView.swift +++ b/mac/Config/Config/IconButtonView.swift @@ -4,13 +4,6 @@ * Created by Gabriel Schantz on 2026-07-03 * * The view used for image-only buttons - * The view relies on the automatically sythesized memberwise initializer - * - * Parameters: - * - action: The closure to perform when the button is clicked - * - systemImage: The String used for the SF Symbol to display - * - font: The Font used for the image - * - helpText: The String used for assistive technologies (e.g. VoiceOver) and shown as a tooltip */ import SwiftUI @@ -22,7 +15,7 @@ public struct IconButtonView: View { let helpText: String public var body: some View { - + Button { action() } label: { diff --git a/mac/Config/Config/KeyboardInfoView.swift b/mac/Config/Config/KeyboardInfoView.swift index 471f5c208fa..e8979c40e3c 100644 --- a/mac/Config/Config/KeyboardInfoView.swift +++ b/mac/Config/Config/KeyboardInfoView.swift @@ -4,7 +4,6 @@ * Created by Gabriel Schantz on 2026-07-20 * * The view used for keyboard info - * The view relies on the automatically synthesized memberwise intializer * * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary */ @@ -87,7 +86,7 @@ public struct KeyboardInfoView: View { VStack { let size: CGFloat = 106 if let qrCode = package.generateSharePackageQRCode(size: size) { - + // the package QR Code Image(nsImage: qrCode) .interpolation(.none) // important: ensures the edges of the QR Code remain sharp @@ -97,7 +96,6 @@ public struct KeyboardInfoView: View { } if let sharePackageUrl = package.sharePackageUrl { - HStack { // the link to share the package online diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index c1b8842f5c1..866eaacedda 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -9,6 +9,7 @@ */ import SwiftUI +import Combine import KeymanSettings struct MainConfigView: View { @@ -33,7 +34,12 @@ struct MainConfigView: View { var body: some View { VStack(spacing: 0) { // the add keyboard button - LabelButtonView(action: { isShowingSheet = true }, label: "Add Keyboard", systemImage: "plus", font: .title2 ) + LabelButtonView( + action: { isShowingSheet = true }, + label: "Add Keyboard", + systemImage: "plus", + font: .title2 + ) .clipShape(.capsule) .padding([.top, .leading, .trailing]) // binds the visibility state to the sheet builder @@ -46,20 +52,38 @@ struct MainConfigView: View { List(settings.singleKeyboardPackages, id: \.id) { package in ForEach(package.keyboards) { keyboard in DisclosureGroup(isExpanded: isExpanded(package: package)) { + // the keyboard info view is shown inside each disclosure group KeyboardInfoView(package: package) } label: { + // the HStack is shown as the label for each disclosure group HStack { Text(keyboard.name) .font(.title) + // the Spacer pushes the other views inside the HStack to the opposite edge Spacer() + // the toggle button for the keyboard + Toggle("enabled", isOn: isEnabled(packageId: package.id, keyboardKey: keyboard.keyboardKey)) + .labelsHidden() + .toggleStyle(.switch) + // see keyboard help button - IconButtonView(action: { print("Show help") }, systemImage: "questionmark.circle", font: .title2, helpText: "Show help") + IconButtonView( + action: { print("Show help") }, + systemImage: "questionmark.circle", + font: .title2, + helpText: "Show help" + ) // delete keyboard button - IconButtonView(action: { showDeleteAlert(for: package) }, systemImage: "trash", font: .title2, helpText: "Delete keyboard") + IconButtonView( + action: { showDeleteAlert(for: package) }, + systemImage: "trash", + font: .title2, + helpText: "Delete keyboard" + ) } .contentShape(Rectangle()) // handles when the HStack is clicked by the user @@ -79,9 +103,9 @@ struct MainConfigView: View { .alert("Are you sure you want to delete the keyboard \"\(selectedPackage?.packageName ?? "")\"?", isPresented: $isShowingDeleteAlert, presenting: selectedPackage) { package in - + // cancel button Button("Cancel", role: .cancel) { } - + // delete button Button("Delete", role: .destructive) { settings.removeInstalledPackage(with: package.id) } @@ -111,7 +135,7 @@ struct MainConfigView: View { // the helper method to generate the custom binding for whether a package's disclosure group is expanded or not func isExpanded(package: KeymanPackage) -> Binding { Binding( - // the getter handles the position of the disclosure group to be rendered + // the getter renders the position of the disclosure group get: { self.expandedPackageID == package.id }, // setter handles when the chevron arrow is clicked by the user // $0 = true when disclosure group is open and $0 = false when disclosure group is closed @@ -119,6 +143,20 @@ struct MainConfigView: View { ) } + // the helper method to generate the custom binding for whether a keyboard is enabled or not + func isEnabled(packageId: UUID, keyboardKey: String) -> Binding { + Binding( + // the getter renders the state of the toggle button based on the enabled property of the keyboard + get: { settings.isKeyboardEnabled(packageId: packageId, keyboardKey: keyboardKey) }, + // the setter handles when the toggle button is clicked by the user + // $0 = true when the toggle button is on and $0 = false when the toggle button is off + set: { + settings.setKeyboardEnabled(packageId: packageId, keyboardKey: keyboardKey, enabled: $0) + settings.objectWillChange.send() + } + ) + } + } #Preview {