From 9441c087170f36c26eef65607aa32d524429eb66 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Mon, 3 Aug 2026 16:29:12 -0400 Subject: [PATCH 01/19] Resolved merge conflict --- 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 7e9875797a..3b8bb555a0 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 d94e5d5f21..5819604243 100644 --- a/mac/Config/Config/ConfigApp.swift +++ b/mac/Config/Config/ConfigApp.swift @@ -24,5 +24,9 @@ struct ConfigApp: App { InstallDebugView() .environmentObject(installation) } + Window("Main Configuration", id: "main_config") { + MainConfigView() + .environmentObject(settings) + } } } 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 0000000000..824775495e --- /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 { + let settings = SettingsContainer() + MainConfigView() + .environmentObject(settings) +} diff --git a/mac/Config/Config/PackageButtonView.swift b/mac/Config/Config/PackageButtonView.swift new file mode 100644 index 0000000000..d67f21fb45 --- /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 8e895e80449077363e9e99632e6d75857b972760 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Mon, 20 Jul 2026 13:15:15 -0400 Subject: [PATCH 02/19] 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 824775495e..2c408f8fea 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 c363a219970b2d0b1caee8854083e57b3b89a57f Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Tue, 21 Jul 2026 15:00:59 -0400 Subject: [PATCH 03/19] 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 d67f21fb45..617b51e324 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 0000000000..55056a7a0f --- /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 2c408f8fea..f68dd97843 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 9bb1c0ba38caa382dafca232ed10f134a114da4f Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Tue, 21 Jul 2026 15:22:47 -0400 Subject: [PATCH 04/19] 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 f68dd97843..0e9a5b4e0a 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 cf80c88a5ea539f4915d8b504c8f7683ec49b7ad Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Tue, 21 Jul 2026 16:02:22 -0400 Subject: [PATCH 05/19] 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 0e9a5b4e0a..d52880405c 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 cbcbdab2cf8d99142e7d2d31d853aeb0679af996 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Thu, 23 Jul 2026 12:32:24 -0400 Subject: [PATCH 06/19] 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 617b51e324..aa268b3947 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 55056a7a0f..601465648d 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 d52880405c..a862be48c7 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 3f0ede2249f400e035a4801b22bc4f363b28f022 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Thu, 23 Jul 2026 16:44:00 -0400 Subject: [PATCH 07/19] 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 601465648d..1ca821f4e4 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 a862be48c7..0e90996be6 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 f7b7e628709ad7c63ab93b0627353d5e0e2ab1ce Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Fri, 24 Jul 2026 11:27:58 -0400 Subject: [PATCH 08/19] 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 aa268b3947..0e62a65d93 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 1ca821f4e4..f8bb564673 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 0e90996be6..b24af0579a 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 c5ded45c065fa8cfb4167b0789a93f49962053c3 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Fri, 24 Jul 2026 15:34:10 -0400 Subject: [PATCH 09/19] 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 f8bb564673..471f5c208f 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 b24af0579a..07eca501ae 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 20c996646b4b5dbe2422a09284a3475a807a9dc9 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Mon, 27 Jul 2026 10:23:57 -0400 Subject: [PATCH 10/19] 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 07eca501ae..e288b0a1cf 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 47aa88b4827c7dbefe22deda4b84f8cb85f12b76 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Mon, 27 Jul 2026 16:09:31 -0400 Subject: [PATCH 11/19] 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 0e62a65d93..6551370c13 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 471f5c208f..e8979c40e3 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 e288b0a1cf..112a505362 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 { From 0cceb76612f60956da385e36a965a1ba1a83e6e2 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Wed, 29 Jul 2026 15:16:50 -0400 Subject: [PATCH 12/19] feat(mac): multi keyboard package views --- mac/Config/Config/IconButtonView.swift | 19 ++- mac/Config/Config/MainConfigView.swift | 146 +++------------- ...rdInfoView.swift => PackageInfoView.swift} | 88 +++++----- mac/Config/Config/PackageRowView.swift | 158 ++++++++++++++++++ 4 files changed, 239 insertions(+), 172 deletions(-) rename mac/Config/Config/{KeyboardInfoView.swift => PackageInfoView.swift} (59%) create mode 100644 mac/Config/Config/PackageRowView.swift diff --git a/mac/Config/Config/IconButtonView.swift b/mac/Config/Config/IconButtonView.swift index 6551370c13..4b42c731f6 100644 --- a/mac/Config/Config/IconButtonView.swift +++ b/mac/Config/Config/IconButtonView.swift @@ -22,10 +22,25 @@ public struct IconButtonView: View { Image(systemName: systemImage) .font(font) } - .buttonStyle(.bordered) - .clipShape(.circle) + .buttonStyle(.plain) .accessibilityLabel(helpText) .help(helpText) } } + +// 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) + } + } +} diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 112a505362..827780654d 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -9,30 +9,16 @@ */ import SwiftUI -import Combine 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 - - /** - * Sets isShowingDeleteAlert to true and assigns the state variable selectedPackage the KeymanPackage argument - */ - private func showDeleteAlert(for package: KeymanPackage) { - isShowingDeleteAlert = true - selectedPackage = package - } var body: some View { - VStack(spacing: 0) { + VStack{ // the add keyboard button LabelButtonView( action: { isShowingSheet = true }, @@ -40,123 +26,29 @@ struct MainConfigView: View { systemImage: "plus", font: .title2 ) - .clipShape(.capsule) - .padding([.top, .leading, .trailing]) + .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: 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" - ) - - // 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 - } - } - } - } + List { + Section { + // the view for single keyboard packages + PackageRowView(packages: settings.singleKeyboardPackages, isSingleKeyboardPackage: true) + } + + Section { + // the view for multi keyboard packages + PackageRowView(packages: settings.multiKeyboardPackages, isSingleKeyboardPackage: false) } } - // 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 - // cancel button - Button("Cancel", role: .cancel) { } - // delete button - Button("Delete", role: .destructive) { - settings.removeInstalledPackage(with: package.id) - } - } message: { package in - Text("You can't undo this action.") - } - .padding([.leading, .trailing, .bottom]) } + .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 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 - set: { self.expandedPackageID = $0 ? package.id : nil } - ) - } - - // 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 { diff --git a/mac/Config/Config/KeyboardInfoView.swift b/mac/Config/Config/PackageInfoView.swift similarity index 59% rename from mac/Config/Config/KeyboardInfoView.swift rename to mac/Config/Config/PackageInfoView.swift index e8979c40e3..cff4590120 100644 --- a/mac/Config/Config/KeyboardInfoView.swift +++ b/mac/Config/Config/PackageInfoView.swift @@ -11,7 +11,7 @@ import SwiftUI import KeymanSettings -public struct KeyboardInfoView: View { +public struct PackageInfoView: View { let package: KeymanPackage /** @@ -34,52 +34,54 @@ public struct KeyboardInfoView: View { .frame(maxWidth: 84, maxHeight: 150) } - // the text-based package properties presented in a grid - Grid(horizontalSpacing: 10, verticalSpacing: 5) { - // the package version - GridRow { - Text("Package Version:").bold() - .gridColumnAlignment(.trailing) // all elements underneath inherit the .trailing alignment - Text(package.packageVersion) - .gridColumnAlignment(.leading) // all elements underneath inherit the .leading alignment - } - - // 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) + VStack { + // the text-based package properties presented in a grid + Grid(horizontalSpacing: 10, verticalSpacing: 5) { + // the package version + GridRow { + Text("Package Version:").bold() + .gridColumnAlignment(.trailing) // all elements underneath inherit the .trailing alignment + Text(package.packageVersion) + .gridColumnAlignment(.leading) // all elements underneath inherit the .leading alignment + } + + // 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) + } } } } + .padding(5) } - .padding(5) - + Spacer() // the package QR Code and link to share the package online diff --git a/mac/Config/Config/PackageRowView.swift b/mac/Config/Config/PackageRowView.swift new file mode 100644 index 0000000000..f795af0a95 --- /dev/null +++ b/mac/Config/Config/PackageRowView.swift @@ -0,0 +1,158 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Gabriel Schantz on 2026-07-27 + * + * View for a package row + */ + +import SwiftUI +import Combine +import KeymanSettings + +public struct PackageRowView: View { + + @EnvironmentObject var settings: SettingsContainer + // 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 + + let packages: [KeymanPackage] + let isSingleKeyboardPackage: Bool + + /** + * Sets isShowingDeleteAlert to true and assigns the state variable selectedPackage the KeymanPackage argument + */ + private func showDeleteAlert(for package: KeymanPackage) { + isShowingDeleteAlert = true + selectedPackage = package + } + + public var body: some View { + ForEach(packages, id: \.id) { package in + ForEach(isSingleKeyboardPackage ? package.keyboards : package.keyboards.onlyFirst) { keyboard in + DisclosureGroup(isExpanded: isExpanded(package: package)) { + // the keyboard info view is shown inside each disclosure group + PackageInfoView(package: package) + } label: { + // the VStack is shown as the label for each disclosure group + VStack (alignment: .leading, spacing: 0) { + HStack { + Text(isSingleKeyboardPackage ? keyboard.name: package.packageName) + .font(.title) + + // the Spacer pushes the other views inside the HStack to the opposite edge + Spacer() + + // the toggle button for the keyboard + if isSingleKeyboardPackage { + Toggle("enabled", isOn: isEnabled(packageId: package.id, keyboardKey: keyboard.keyboardKey)) + .controlSize(.mini) + .labelsHidden() + .toggleStyle(.switch) + .gridColumnAlignment(.leading) + } + + // see keyboard help button + IconButtonView( + action: { print("View keyboard help") }, + systemImage: "info.circle", + font: .title2, + helpText: "View keyboard help" + ) + + // delete keyboard button + IconButtonView( + action: { showDeleteAlert(for: package) }, + systemImage: "trash", + font: .title2, + helpText: "Delete keyboard" + ) + } + + if !isSingleKeyboardPackage { + ForEach (package.keyboards) { keyboard in + HStack { + Text(keyboard.name) + .font(.title3) + .foregroundStyle(.secondary) + .gridColumnAlignment(.leading) + + // 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)) + .controlSize(.mini) + .labelsHidden() + .toggleStyle(.switch) + .gridColumnAlignment(.leading) + } + } + } + } + .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 + // cancel button + Button("Cancel", role: .cancel) { } + // delete button + Button("Delete", role: .destructive) { + settings.removeInstalledPackage(with: package.id) + } + } message: { package in + Text("You can't undo this action.") + } + } + + // 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 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 + set: { self.expandedPackageID = $0 ? package.id : nil } + ) + } + + // 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() + } + ) + } +} + +extension Collection { + // returns the first element of an array in an array or returns an empty array + var onlyFirst: [Element] { + guard let first = self.first else { return [] } + return [first] + } +} From a2940f3ec7ad08e4c27e903751679fc6064bcb36 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Thu, 30 Jul 2026 11:13:30 -0400 Subject: [PATCH 13/19] feat(mac): disclosure groups share state variable and style changes --- mac/Config/Config/MainConfigView.swift | 24 +++++++++++------- mac/Config/Config/PackageRowView.swift | 35 ++++++++++++++++---------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 827780654d..33194e1667 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -16,9 +16,12 @@ struct MainConfigView: View { @EnvironmentObject var settings: SettingsContainer // visibilty state for the add package sheet @State private var isShowingSheet = false + // used to identify the expanded KeymanPackage id + // both single and multi package views share the same state variable so only single disclosure group is expanded at once + @State private var expandedPackageID: UUID? = nil var body: some View { - VStack{ + VStack { // the add keyboard button LabelButtonView( action: { isShowingSheet = true }, @@ -34,17 +37,20 @@ struct MainConfigView: View { .frame(width: 960, height: 390) // FEAT/MAC/CONFIG-WINDOW TODO: Make width and height percentages } - - List { - Section { + ScrollView { + VStack { // the view for single keyboard packages - PackageRowView(packages: settings.singleKeyboardPackages, isSingleKeyboardPackage: true) - } - - Section { + PackageRowView(packages: settings.singleKeyboardPackages, isSingleKeyboardPackage: true, expandedPackageID: $expandedPackageID) + // the view for multi keyboard packages - PackageRowView(packages: settings.multiKeyboardPackages, isSingleKeyboardPackage: false) + PackageRowView(packages: settings.multiKeyboardPackages, isSingleKeyboardPackage: false, expandedPackageID: $expandedPackageID) } + .padding() + .background(.quaternary) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + + // the Spacer pushes the contents of the VStack to the top of the VStack + Spacer() } } .padding([.leading, .trailing, .bottom]) diff --git a/mac/Config/Config/PackageRowView.swift b/mac/Config/Config/PackageRowView.swift index f795af0a95..55a648e1c0 100644 --- a/mac/Config/Config/PackageRowView.swift +++ b/mac/Config/Config/PackageRowView.swift @@ -17,11 +17,18 @@ public struct PackageRowView: View { @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 + // settings.singleKeyboardPackages or settings.multiKeyboardPackages let packages: [KeymanPackage] let isSingleKeyboardPackage: Bool + // binded to the shared state variable in the parent view + @Binding var expandedPackageID: UUID? + + init(packages: [KeymanPackage], isSingleKeyboardPackage: Bool, expandedPackageID: Binding) { + self.packages = packages + self.isSingleKeyboardPackage = isSingleKeyboardPackage + self._expandedPackageID = expandedPackageID + } /** * Sets isShowingDeleteAlert to true and assigns the state variable selectedPackage the KeymanPackage argument @@ -35,19 +42,20 @@ public struct PackageRowView: View { ForEach(packages, id: \.id) { package in ForEach(isSingleKeyboardPackage ? package.keyboards : package.keyboards.onlyFirst) { keyboard in DisclosureGroup(isExpanded: isExpanded(package: package)) { - // the keyboard info view is shown inside each disclosure group + // the package info view is shown inside each disclosure group PackageInfoView(package: package) } label: { - // the VStack is shown as the label for each disclosure group + // a VStack is shown as the label for each disclosure group VStack (alignment: .leading, spacing: 0) { HStack { + // if the package contains one keyboard shows the keyboard name, otherwise show the package name Text(isSingleKeyboardPackage ? keyboard.name: package.packageName) .font(.title) - // the Spacer pushes the other views inside the HStack to the opposite edge + // the Spacer pushes the contents of the HStack to the either edge Spacer() - // the toggle button for the keyboard + // if the package contains one keyboard shows the toggle button for the keyboard if isSingleKeyboardPackage { Toggle("enabled", isOn: isEnabled(packageId: package.id, keyboardKey: keyboard.keyboardKey)) .controlSize(.mini) @@ -58,10 +66,10 @@ public struct PackageRowView: View { // see keyboard help button IconButtonView( - action: { print("View keyboard help") }, - systemImage: "info.circle", + action: { print("Show keyboard help") }, + systemImage: "questionmark.circle", font: .title2, - helpText: "View keyboard help" + helpText: "Show keyboard help" ) // delete keyboard button @@ -73,12 +81,13 @@ public struct PackageRowView: View { ) } + // if the package contains multiple keyboards shows an HStack with the keyboard name and toggle button for each keyboard in the package if !isSingleKeyboardPackage { ForEach (package.keyboards) { keyboard in HStack { Text(keyboard.name) - .font(.title3) - .foregroundStyle(.secondary) + .font(.title2) + .foregroundStyle(.primary) .gridColumnAlignment(.leading) // the Spacer pushes the other views inside the HStack to the opposite edge @@ -127,10 +136,10 @@ public struct PackageRowView: View { func isExpanded(package: KeymanPackage) -> Binding { Binding( // the getter renders the position of the disclosure group - get: { self.expandedPackageID == package.id }, + get: { 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 } + set: { expandedPackageID = $0 ? package.id : nil } ) } From f0fa17f5e51a5b3a7a4f411e84ffdf17547321a5 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Thu, 30 Jul 2026 15:00:57 -0400 Subject: [PATCH 14/19] feat(mac): fixed package info animation --- mac/Config/Config/PackageRowView.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mac/Config/Config/PackageRowView.swift b/mac/Config/Config/PackageRowView.swift index 55a648e1c0..9591dc409b 100644 --- a/mac/Config/Config/PackageRowView.swift +++ b/mac/Config/Config/PackageRowView.swift @@ -43,7 +43,10 @@ public struct PackageRowView: View { ForEach(isSingleKeyboardPackage ? package.keyboards : package.keyboards.onlyFirst) { keyboard in DisclosureGroup(isExpanded: isExpanded(package: package)) { // the package info view is shown inside each disclosure group - PackageInfoView(package: package) + if expandedPackageID == package.id { + PackageInfoView(package: package) + .transition(.move(edge: .top)) + } } label: { // a VStack is shown as the label for each disclosure group VStack (alignment: .leading, spacing: 0) { @@ -139,7 +142,10 @@ public struct PackageRowView: View { get: { 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: { expandedPackageID = $0 ? package.id : nil } + set: { isExpanded in + withAnimation { + expandedPackageID = isExpanded ? package.id : nil } + } ) } From 5c59da838b4901017f85f3b52c3f138fdc308ba5 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Thu, 30 Jul 2026 15:14:10 -0400 Subject: [PATCH 15/19] feat(mac): resolved changes from applying stash --- mac/Config/Config/MainConfigView.swift | 2 +- mac/Config/Config/PackageInfoView.swift | 14 ++++++++++++-- mac/Config/Config/PackageRowView.swift | 16 +++++----------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 33194e1667..427ea2c498 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -46,7 +46,7 @@ struct MainConfigView: View { PackageRowView(packages: settings.multiKeyboardPackages, isSingleKeyboardPackage: false, expandedPackageID: $expandedPackageID) } .padding() - .background(.quaternary) + .background(.quinary) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) // the Spacer pushes the contents of the VStack to the top of the VStack diff --git a/mac/Config/Config/PackageInfoView.swift b/mac/Config/Config/PackageInfoView.swift index cff4590120..28638f5e46 100644 --- a/mac/Config/Config/PackageInfoView.swift +++ b/mac/Config/Config/PackageInfoView.swift @@ -12,8 +12,10 @@ import SwiftUI import KeymanSettings public struct PackageInfoView: View { - let package: KeymanPackage + let package: KeymanPackage + // closure passed from the parent view + let showAlertFunction: (KeymanPackage) -> Void /** * Copies the text argument to the system clipboard */ @@ -34,7 +36,7 @@ public struct PackageInfoView: View { .frame(maxWidth: 84, maxHeight: 150) } - VStack { + VStack (alignment: .leading) { // the text-based package properties presented in a grid Grid(horizontalSpacing: 10, verticalSpacing: 5) { // the package version @@ -80,6 +82,14 @@ public struct PackageInfoView: View { } } .padding(5) + + // uninstall button + Button { + showAlertFunction(package) + } label: { + Text("Remove Package") + } + .padding(.horizontal) } Spacer() diff --git a/mac/Config/Config/PackageRowView.swift b/mac/Config/Config/PackageRowView.swift index 9591dc409b..aa1d813a83 100644 --- a/mac/Config/Config/PackageRowView.swift +++ b/mac/Config/Config/PackageRowView.swift @@ -33,7 +33,7 @@ public struct PackageRowView: View { /** * Sets isShowingDeleteAlert to true and assigns the state variable selectedPackage the KeymanPackage argument */ - private func showDeleteAlert(for package: KeymanPackage) { + public func showDeleteAlert(for package: KeymanPackage) { isShowingDeleteAlert = true selectedPackage = package } @@ -44,7 +44,9 @@ public struct PackageRowView: View { DisclosureGroup(isExpanded: isExpanded(package: package)) { // the package info view is shown inside each disclosure group if expandedPackageID == package.id { - PackageInfoView(package: package) + PackageInfoView(package: package, showAlertFunction: { package in + showDeleteAlert(for: package) + }) .transition(.move(edge: .top)) } } label: { @@ -74,14 +76,6 @@ public struct PackageRowView: View { font: .title2, helpText: "Show keyboard help" ) - - // delete keyboard button - IconButtonView( - action: { showDeleteAlert(for: package) }, - systemImage: "trash", - font: .title2, - helpText: "Delete keyboard" - ) } // if the package contains multiple keyboards shows an HStack with the keyboard name and toggle button for each keyboard in the package @@ -109,7 +103,7 @@ public struct PackageRowView: View { .contentShape(Rectangle()) // handles when the HStack is clicked by the user .onTapGesture { - withAnimation { + withAnimation () { if self.expandedPackageID == package.id { self.expandedPackageID = nil } else { From 2b2fc15da30adb047c32871d3fe0feb70ca9a377 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Mon, 3 Aug 2026 15:37:35 -0400 Subject: [PATCH 16/19] feat(mac): added tabview and webview --- mac/Config/Config/HelpView.swift | 34 +++++++++++ mac/Config/Config/MainConfigView.swift | 78 ++++++++++++++++--------- mac/Config/Config/PackageInfoView.swift | 1 + mac/Config/Config/PackageRowView.swift | 24 +++++--- 4 files changed, 102 insertions(+), 35 deletions(-) create mode 100644 mac/Config/Config/HelpView.swift diff --git a/mac/Config/Config/HelpView.swift b/mac/Config/Config/HelpView.swift new file mode 100644 index 0000000000..f30e46014c --- /dev/null +++ b/mac/Config/Config/HelpView.swift @@ -0,0 +1,34 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Gabriel Schantz on 2026-08-03 + * + * Webview used to show help for Keyman keyboards + */ +import Foundation + +import SwiftUI +import WebKit +import KeymanSettings + +public struct HelpView: NSViewRepresentable { + @EnvironmentObject var settings: SettingsContainer + + let helpFileURL: URL + + // create the AppKit view instance + public func makeNSView(context: Context) -> WKWebView { + let webView = WKWebView() + return webView + } + + // update the view when SwiftUI state changes + public func updateNSView(_ nsView: WKWebView, context: Context) { + let request = URLRequest(url: helpFileURL) + + // only load the request if it's not already loading/loaded to prevent infinite loops + if nsView.url != helpFileURL { + nsView.load(request) + } + } +} diff --git a/mac/Config/Config/MainConfigView.swift b/mac/Config/Config/MainConfigView.swift index 427ea2c498..2a24491853 100644 --- a/mac/Config/Config/MainConfigView.swift +++ b/mac/Config/Config/MainConfigView.swift @@ -4,8 +4,7 @@ * Created by Gabriel Schantz on 2026-06-29 * * Main view used for configuring Keyman - * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary - * FEAT/MAC/CONFIG-WINDOW TODO: Set width and height for window + * FEAT/MAC/CONFIG-WINDOW TODO: Set default width and height for window */ import SwiftUI @@ -17,43 +16,68 @@ struct MainConfigView: View { // visibilty state for the add package sheet @State private var isShowingSheet = false // used to identify the expanded KeymanPackage id - // both single and multi package views share the same state variable so only single disclosure group is expanded at once + // both single and multi package views share the same state variable so only a single disclosure group is expanded at once @State private var expandedPackageID: UUID? = nil + @State private var selectedTab = 0 + @State private var packageSelectedForHelpUrl: URL? = nil + + /** + * Assigns packageSelectedForHelpUrl the url argument and changes the selected tab to the help tab + */ + public func showHelpTab(for url: URL) { + packageSelectedForHelpUrl = url + selectedTab = 1 + } var body: some View { - VStack { - // 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 - } - ScrollView { - VStack { + TabView (selection: $selectedTab) { + VStack { + // 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 + } + + Form { // the view for single keyboard packages - PackageRowView(packages: settings.singleKeyboardPackages, isSingleKeyboardPackage: true, expandedPackageID: $expandedPackageID) + PackageRowView(packages: settings.singleKeyboardPackages, isSingleKeyboardPackage: true, expandedPackageID: $expandedPackageID, showHelpTab: { url in + showHelpTab(for: url)}) // the view for multi keyboard packages - PackageRowView(packages: settings.multiKeyboardPackages, isSingleKeyboardPackage: false, expandedPackageID: $expandedPackageID) + PackageRowView(packages: settings.multiKeyboardPackages, isSingleKeyboardPackage: false, expandedPackageID: $expandedPackageID, showHelpTab: { url in + showHelpTab(for: url) }) } - .padding() - .background(.quinary) - .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + .formStyle(.grouped) // the Spacer pushes the contents of the VStack to the top of the VStack Spacer() } + .padding([.leading, .trailing, .bottom]) + .tabItem { Text("Keyboards") } + .tag(0) + + if let url = packageSelectedForHelpUrl { + HelpView(helpFileURL: url) + .padding() + .tabItem { Text("Help") } + .tag(1) + } else { + Text("Help not available.") + .font(.title) + .tabItem { Text("Help") } + .tag(1) + } } - .padding([.leading, .trailing, .bottom]) } } diff --git a/mac/Config/Config/PackageInfoView.swift b/mac/Config/Config/PackageInfoView.swift index 28638f5e46..8c3d19bdcd 100644 --- a/mac/Config/Config/PackageInfoView.swift +++ b/mac/Config/Config/PackageInfoView.swift @@ -39,6 +39,7 @@ public struct PackageInfoView: View { VStack (alignment: .leading) { // the text-based package properties presented in a grid Grid(horizontalSpacing: 10, verticalSpacing: 5) { + // the package version GridRow { Text("Package Version:").bold() diff --git a/mac/Config/Config/PackageRowView.swift b/mac/Config/Config/PackageRowView.swift index aa1d813a83..4d5e74ac20 100644 --- a/mac/Config/Config/PackageRowView.swift +++ b/mac/Config/Config/PackageRowView.swift @@ -20,14 +20,18 @@ public struct PackageRowView: View { // settings.singleKeyboardPackages or settings.multiKeyboardPackages let packages: [KeymanPackage] + // a boolean for weather or not a package contains multiple keyboards let isSingleKeyboardPackage: Bool // binded to the shared state variable in the parent view @Binding var expandedPackageID: UUID? + // closure passed from the parent view + let showHelpTab: (URL) -> Void - init(packages: [KeymanPackage], isSingleKeyboardPackage: Bool, expandedPackageID: Binding) { + init(packages: [KeymanPackage], isSingleKeyboardPackage: Bool, expandedPackageID: Binding, showHelpTab: @escaping (URL) -> Void) { self.packages = packages self.isSingleKeyboardPackage = isSingleKeyboardPackage self._expandedPackageID = expandedPackageID + self.showHelpTab = showHelpTab } /** @@ -57,6 +61,16 @@ public struct PackageRowView: View { Text(isSingleKeyboardPackage ? keyboard.name: package.packageName) .font(.title) + // see keyboard help button + if let url = package.helpFileUrl { + IconButtonView( + action: { showHelpTab(url) }, + systemImage: "questionmark.circle", + font: .title2, + helpText: "Show keyboard help" + ) + } + // the Spacer pushes the contents of the HStack to the either edge Spacer() @@ -69,13 +83,7 @@ public struct PackageRowView: View { .gridColumnAlignment(.leading) } - // see keyboard help button - IconButtonView( - action: { print("Show keyboard help") }, - systemImage: "questionmark.circle", - font: .title2, - helpText: "Show keyboard help" - ) + } // if the package contains multiple keyboards shows an HStack with the keyboard name and toggle button for each keyboard in the package From 3a193877be82efe5a54a302613f26873ecda8a8f Mon Sep 17 00:00:00 2001 From: geschantz Date: Mon, 10 Aug 2026 21:08:06 -0400 Subject: [PATCH 17/19] Update mac/Config/Config/PackageRowView.swift Co-authored-by: Shawn Schantz <89134789+sgschantz@users.noreply.github.com> --- mac/Config/Config/PackageRowView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mac/Config/Config/PackageRowView.swift b/mac/Config/Config/PackageRowView.swift index 4d5e74ac20..9f5fe3da17 100644 --- a/mac/Config/Config/PackageRowView.swift +++ b/mac/Config/Config/PackageRowView.swift @@ -57,7 +57,7 @@ public struct PackageRowView: View { // a VStack is shown as the label for each disclosure group VStack (alignment: .leading, spacing: 0) { HStack { - // if the package contains one keyboard shows the keyboard name, otherwise show the package name + // if the package contains one keyboard, show the keyboard name, otherwise show the package name Text(isSingleKeyboardPackage ? keyboard.name: package.packageName) .font(.title) From f01050ca8e540e05e9591c9fe3c488551719e7ad Mon Sep 17 00:00:00 2001 From: geschantz Date: Mon, 10 Aug 2026 21:08:51 -0400 Subject: [PATCH 18/19] Update mac/Config/Config/PackageRowView.swift Co-authored-by: Shawn Schantz <89134789+sgschantz@users.noreply.github.com> --- mac/Config/Config/PackageRowView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mac/Config/Config/PackageRowView.swift b/mac/Config/Config/PackageRowView.swift index 9f5fe3da17..71fccdeb5a 100644 --- a/mac/Config/Config/PackageRowView.swift +++ b/mac/Config/Config/PackageRowView.swift @@ -20,7 +20,7 @@ public struct PackageRowView: View { // settings.singleKeyboardPackages or settings.multiKeyboardPackages let packages: [KeymanPackage] - // a boolean for weather or not a package contains multiple keyboards + // a boolean for whether or not a package contains multiple keyboards let isSingleKeyboardPackage: Bool // binded to the shared state variable in the parent view @Binding var expandedPackageID: UUID? From c9915dd6c239c1f52020edb6927672e248403150 Mon Sep 17 00:00:00 2001 From: Gabriel Schantz Date: Mon, 10 Aug 2026 21:23:12 -0400 Subject: [PATCH 19/19] feat(mac): responded to review comments --- mac/Config/Config/PackageInfoView.swift | 4 +--- mac/Config/Config/PackageRowView.swift | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mac/Config/Config/PackageInfoView.swift b/mac/Config/Config/PackageInfoView.swift index 8c3d19bdcd..413fc2abd7 100644 --- a/mac/Config/Config/PackageInfoView.swift +++ b/mac/Config/Config/PackageInfoView.swift @@ -3,9 +3,7 @@ * * Created by Gabriel Schantz on 2026-07-20 * - * The view used for keyboard info - * - * FEAT/MAC/CONFIG-WINDOW TODO: Finish writing file summary + * The view used to display package info inside the disclosure group opened from PackageRowView */ import SwiftUI diff --git a/mac/Config/Config/PackageRowView.swift b/mac/Config/Config/PackageRowView.swift index 71fccdeb5a..b607412636 100644 --- a/mac/Config/Config/PackageRowView.swift +++ b/mac/Config/Config/PackageRowView.swift @@ -3,7 +3,9 @@ * * Created by Gabriel Schantz on 2026-07-27 * - * View for a package row + * Displays an array of Keyman Packages as disclosure groups + * If the argument isSingleKeyboardPackage is true, displays information in a format suited to a single keyboard, + * otherwise displays information in a format suited to a multi-keyboard package */ import SwiftUI